mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
feat: support memory import and export (#1521)
* feat: support memory import and export * fix(memory): address review feedback * style: format memory settings page --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
2330c38209
commit
9a557751d6
@ -8,6 +8,7 @@ from deerflow.agents.memory.updater import (
|
|||||||
create_memory_fact,
|
create_memory_fact,
|
||||||
delete_memory_fact,
|
delete_memory_fact,
|
||||||
get_memory_data,
|
get_memory_data,
|
||||||
|
import_memory_data,
|
||||||
reload_memory_data,
|
reload_memory_data,
|
||||||
update_memory_fact,
|
update_memory_fact,
|
||||||
)
|
)
|
||||||
@ -248,6 +249,34 @@ async def update_memory_fact_endpoint(fact_id: str, request: FactPatchRequest) -
|
|||||||
return MemoryResponse(**memory_data)
|
return MemoryResponse(**memory_data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/memory/export",
|
||||||
|
response_model=MemoryResponse,
|
||||||
|
summary="Export Memory Data",
|
||||||
|
description="Export the current global memory data as JSON for backup or transfer.",
|
||||||
|
)
|
||||||
|
async def export_memory() -> MemoryResponse:
|
||||||
|
"""Export the current memory data."""
|
||||||
|
memory_data = get_memory_data()
|
||||||
|
return MemoryResponse(**memory_data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/memory/import",
|
||||||
|
response_model=MemoryResponse,
|
||||||
|
summary="Import Memory Data",
|
||||||
|
description="Import and overwrite the current global memory data from a JSON payload.",
|
||||||
|
)
|
||||||
|
async def import_memory(request: MemoryResponse) -> MemoryResponse:
|
||||||
|
"""Import and persist memory data."""
|
||||||
|
try:
|
||||||
|
memory_data = import_memory_data(request.model_dump())
|
||||||
|
except OSError as exc:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to import memory data.") from exc
|
||||||
|
|
||||||
|
return MemoryResponse(**memory_data)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/memory/config",
|
"/memory/config",
|
||||||
response_model=MemoryConfigResponse,
|
response_model=MemoryConfigResponse,
|
||||||
|
|||||||
@ -39,6 +39,25 @@ def reload_memory_data(agent_name: str | None = None) -> dict[str, Any]:
|
|||||||
return get_memory_storage().reload(agent_name)
|
return get_memory_storage().reload(agent_name)
|
||||||
|
|
||||||
|
|
||||||
|
def import_memory_data(memory_data: dict[str, Any], agent_name: str | None = None) -> dict[str, Any]:
|
||||||
|
"""Persist imported memory data via storage provider.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
memory_data: Full memory payload to persist.
|
||||||
|
agent_name: If provided, imports into per-agent memory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The saved memory data after storage normalization.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OSError: If persisting the imported memory fails.
|
||||||
|
"""
|
||||||
|
storage = get_memory_storage()
|
||||||
|
if not storage.save(memory_data, agent_name):
|
||||||
|
raise OSError("Failed to save imported memory data")
|
||||||
|
return storage.load(agent_name)
|
||||||
|
|
||||||
|
|
||||||
def clear_memory_data(agent_name: str | None = None) -> dict[str, Any]:
|
def clear_memory_data(agent_name: str | None = None) -> dict[str, Any]:
|
||||||
"""Clear all stored memory data and persist an empty structure."""
|
"""Clear all stored memory data and persist an empty structure."""
|
||||||
cleared_memory = create_empty_memory()
|
cleared_memory = create_empty_memory()
|
||||||
|
|||||||
@ -507,6 +507,18 @@ class DeerFlowClient:
|
|||||||
|
|
||||||
return get_memory_data()
|
return get_memory_data()
|
||||||
|
|
||||||
|
def export_memory(self) -> dict:
|
||||||
|
"""Export current memory data for backup or transfer."""
|
||||||
|
from deerflow.agents.memory.updater import get_memory_data
|
||||||
|
|
||||||
|
return get_memory_data()
|
||||||
|
|
||||||
|
def import_memory(self, memory_data: dict) -> dict:
|
||||||
|
"""Import and persist full memory data."""
|
||||||
|
from deerflow.agents.memory.updater import import_memory_data
|
||||||
|
|
||||||
|
return import_memory_data(memory_data)
|
||||||
|
|
||||||
def get_model(self, name: str) -> dict | None:
|
def get_model(self, name: str) -> dict | None:
|
||||||
"""Get a specific model's configuration by name.
|
"""Get a specific model's configuration by name.
|
||||||
|
|
||||||
|
|||||||
@ -145,6 +145,13 @@ class TestConfigQueries:
|
|||||||
mock_mem.assert_called_once()
|
mock_mem.assert_called_once()
|
||||||
assert result == memory
|
assert result == memory
|
||||||
|
|
||||||
|
def test_export_memory(self, client):
|
||||||
|
memory = {"version": "1.0", "facts": []}
|
||||||
|
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=memory) as mock_mem:
|
||||||
|
result = client.export_memory()
|
||||||
|
mock_mem.assert_called_once()
|
||||||
|
assert result == memory
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# stream / chat
|
# stream / chat
|
||||||
@ -661,6 +668,14 @@ class TestSkillsManagement:
|
|||||||
|
|
||||||
|
|
||||||
class TestMemoryManagement:
|
class TestMemoryManagement:
|
||||||
|
def test_import_memory(self, client):
|
||||||
|
imported = {"version": "1.0", "facts": []}
|
||||||
|
with patch("deerflow.agents.memory.updater.import_memory_data", return_value=imported) as mock_import:
|
||||||
|
result = client.import_memory(imported)
|
||||||
|
|
||||||
|
mock_import.assert_called_once_with(imported)
|
||||||
|
assert result == imported
|
||||||
|
|
||||||
def test_reload_memory(self, client):
|
def test_reload_memory(self, client):
|
||||||
data = {"version": "1.0", "facts": []}
|
data = {"version": "1.0", "facts": []}
|
||||||
with patch("deerflow.agents.memory.updater.reload_memory_data", return_value=data):
|
with patch("deerflow.agents.memory.updater.reload_memory_data", return_value=data):
|
||||||
|
|||||||
@ -24,6 +24,54 @@ def _sample_memory(facts: list[dict] | None = None) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_memory_route_returns_current_memory() -> None:
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(memory.router)
|
||||||
|
exported_memory = _sample_memory(
|
||||||
|
facts=[
|
||||||
|
{
|
||||||
|
"id": "fact_export",
|
||||||
|
"content": "User prefers concise responses.",
|
||||||
|
"category": "preference",
|
||||||
|
"confidence": 0.9,
|
||||||
|
"createdAt": "2026-03-20T00:00:00Z",
|
||||||
|
"source": "thread-1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("app.gateway.routers.memory.get_memory_data", return_value=exported_memory):
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.get("/api/memory/export")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["facts"] == exported_memory["facts"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_memory_route_returns_imported_memory() -> None:
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(memory.router)
|
||||||
|
imported_memory = _sample_memory(
|
||||||
|
facts=[
|
||||||
|
{
|
||||||
|
"id": "fact_import",
|
||||||
|
"content": "User works on DeerFlow.",
|
||||||
|
"category": "context",
|
||||||
|
"confidence": 0.87,
|
||||||
|
"createdAt": "2026-03-20T00:00:00Z",
|
||||||
|
"source": "manual",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("app.gateway.routers.memory.import_memory_data", return_value=imported_memory):
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.post("/api/memory/import", json=imported_memory)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["facts"] == imported_memory["facts"]
|
||||||
|
|
||||||
|
|
||||||
def test_clear_memory_route_returns_cleared_memory() -> None:
|
def test_clear_memory_route_returns_cleared_memory() -> None:
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(memory.router)
|
app.include_router(memory.router)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from deerflow.agents.memory.updater import (
|
|||||||
clear_memory_data,
|
clear_memory_data,
|
||||||
create_memory_fact,
|
create_memory_fact,
|
||||||
delete_memory_fact,
|
delete_memory_fact,
|
||||||
|
import_memory_data,
|
||||||
update_memory_fact,
|
update_memory_fact,
|
||||||
)
|
)
|
||||||
from deerflow.config.memory_config import MemoryConfig
|
from deerflow.config.memory_config import MemoryConfig
|
||||||
@ -233,6 +234,31 @@ def test_delete_memory_fact_raises_for_unknown_id() -> None:
|
|||||||
raise AssertionError("Expected KeyError for missing fact id")
|
raise AssertionError("Expected KeyError for missing fact id")
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_memory_data_saves_and_returns_imported_memory() -> None:
|
||||||
|
imported_memory = _make_memory(
|
||||||
|
facts=[
|
||||||
|
{
|
||||||
|
"id": "fact_import",
|
||||||
|
"content": "User works on DeerFlow.",
|
||||||
|
"category": "context",
|
||||||
|
"confidence": 0.87,
|
||||||
|
"createdAt": "2026-03-20T00:00:00Z",
|
||||||
|
"source": "manual",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
mock_storage = MagicMock()
|
||||||
|
mock_storage.save.return_value = True
|
||||||
|
mock_storage.load.return_value = imported_memory
|
||||||
|
|
||||||
|
with patch("deerflow.agents.memory.updater.get_memory_storage", return_value=mock_storage):
|
||||||
|
result = import_memory_data(imported_memory)
|
||||||
|
|
||||||
|
mock_storage.save.assert_called_once_with(imported_memory, None)
|
||||||
|
mock_storage.load.assert_called_once_with(None)
|
||||||
|
assert result == imported_memory
|
||||||
|
|
||||||
|
|
||||||
def test_update_memory_fact_updates_only_matching_fact() -> None:
|
def test_update_memory_fact_updates_only_matching_fact() -> None:
|
||||||
current_memory = _make_memory(
|
current_memory = _make_memory(
|
||||||
facts=[
|
facts=[
|
||||||
@ -349,7 +375,7 @@ def test_update_memory_fact_rejects_invalid_confidence() -> None:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _extract_text — LLM response content normalization
|
# _extract_text - LLM response content normalization
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@ -409,7 +435,7 @@ class TestExtractText:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# format_conversation_for_update — handles mixed list content
|
# format_conversation_for_update - handles mixed list content
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@ -439,7 +465,7 @@ class TestFormatConversationForUpdate:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# update_memory — structured LLM response handling
|
# update_memory - structured LLM response handling
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
55
frontend/src/app/api/memory/[...path]/route.ts
Normal file
55
frontend/src/app/api/memory/[...path]/route.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
const BACKEND_BASE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://127.0.0.1:8001";
|
||||||
|
|
||||||
|
function buildBackendUrl(pathname: string) {
|
||||||
|
return new URL(pathname, BACKEND_BASE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyRequest(request: NextRequest, pathname: string) {
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.delete("host");
|
||||||
|
headers.delete("connection");
|
||||||
|
headers.delete("content-length");
|
||||||
|
|
||||||
|
const hasBody = !["GET", "HEAD"].includes(request.method);
|
||||||
|
const response = await fetch(buildBackendUrl(pathname), {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: hasBody ? await request.arrayBuffer() : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(await response.arrayBuffer(), {
|
||||||
|
status: response.status,
|
||||||
|
headers: response.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ path: string[] }> },
|
||||||
|
) {
|
||||||
|
return proxyRequest(request, `/api/memory/${(await params).path.join("/")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ path: string[] }> },
|
||||||
|
) {
|
||||||
|
return proxyRequest(request, `/api/memory/${(await params).path.join("/")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ path: string[] }> },
|
||||||
|
) {
|
||||||
|
return proxyRequest(request, `/api/memory/${(await params).path.join("/")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ path: string[] }> },
|
||||||
|
) {
|
||||||
|
return proxyRequest(request, `/api/memory/${(await params).path.join("/")}`);
|
||||||
|
}
|
||||||
35
frontend/src/app/api/memory/route.ts
Normal file
35
frontend/src/app/api/memory/route.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
const BACKEND_BASE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://127.0.0.1:8010";
|
||||||
|
|
||||||
|
function buildBackendUrl(pathname: string) {
|
||||||
|
return new URL(pathname, BACKEND_BASE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyRequest(request: NextRequest, pathname: string) {
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.delete("host");
|
||||||
|
headers.delete("connection");
|
||||||
|
headers.delete("content-length");
|
||||||
|
|
||||||
|
const hasBody = !["GET", "HEAD"].includes(request.method);
|
||||||
|
const response = await fetch(buildBackendUrl(pathname), {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: hasBody ? await request.arrayBuffer() : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(await response.arrayBuffer(), {
|
||||||
|
status: response.status,
|
||||||
|
headers: response.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
return proxyRequest(request, "/api/memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
return proxyRequest(request, "/api/memory");
|
||||||
|
}
|
||||||
@ -1,8 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PenLineIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
import {
|
||||||
|
DownloadIcon,
|
||||||
|
PenLineIcon,
|
||||||
|
PlusIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
UploadIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useDeferredValue, useId, useState } from "react";
|
import { useDeferredValue, useId, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
@ -19,10 +25,12 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { exportMemory } from "@/core/memory/api";
|
||||||
import {
|
import {
|
||||||
useClearMemory,
|
useClearMemory,
|
||||||
useCreateMemoryFact,
|
useCreateMemoryFact,
|
||||||
useDeleteMemoryFact,
|
useDeleteMemoryFact,
|
||||||
|
useImportMemory,
|
||||||
useMemory,
|
useMemory,
|
||||||
useUpdateMemoryFact,
|
useUpdateMemoryFact,
|
||||||
} from "@/core/memory/hooks";
|
} from "@/core/memory/hooks";
|
||||||
@ -51,6 +59,65 @@ type MemorySectionGroup = {
|
|||||||
sections: MemorySection[];
|
sections: MemorySection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PendingImport = {
|
||||||
|
fileName: string;
|
||||||
|
memory: UserMemory;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMemorySection(value: unknown): value is {
|
||||||
|
summary: string;
|
||||||
|
updatedAt: string;
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
isRecord(value) &&
|
||||||
|
typeof value.summary === "string" &&
|
||||||
|
typeof value.updatedAt === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMemoryFact(value: unknown): value is UserMemory["facts"][number] {
|
||||||
|
return (
|
||||||
|
isRecord(value) &&
|
||||||
|
typeof value.id === "string" &&
|
||||||
|
typeof value.content === "string" &&
|
||||||
|
typeof value.category === "string" &&
|
||||||
|
typeof value.confidence === "number" &&
|
||||||
|
Number.isFinite(value.confidence) &&
|
||||||
|
typeof value.createdAt === "string" &&
|
||||||
|
typeof value.source === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImportedMemory(value: unknown): value is UserMemory {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof value.version !== "string" ||
|
||||||
|
typeof value.lastUpdated !== "string" ||
|
||||||
|
!isRecord(value.user) ||
|
||||||
|
!isRecord(value.history) ||
|
||||||
|
!Array.isArray(value.facts)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isMemorySection(value.user.workContext) &&
|
||||||
|
isMemorySection(value.user.personalContext) &&
|
||||||
|
isMemorySection(value.user.topOfMind) &&
|
||||||
|
isMemorySection(value.history.recentMonths) &&
|
||||||
|
isMemorySection(value.history.earlierContext) &&
|
||||||
|
isMemorySection(value.history.longTermBackground) &&
|
||||||
|
value.facts.every(isMemoryFact)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type FactFormState = {
|
type FactFormState = {
|
||||||
content: string;
|
content: string;
|
||||||
category: string;
|
category: string;
|
||||||
@ -212,6 +279,8 @@ export function MemorySettingsPage() {
|
|||||||
const clearMemory = useClearMemory();
|
const clearMemory = useClearMemory();
|
||||||
const createMemoryFact = useCreateMemoryFact();
|
const createMemoryFact = useCreateMemoryFact();
|
||||||
const deleteMemoryFact = useDeleteMemoryFact();
|
const deleteMemoryFact = useDeleteMemoryFact();
|
||||||
|
const importMemoryMutation = useImportMemory();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const updateMemoryFact = useUpdateMemoryFact();
|
const updateMemoryFact = useUpdateMemoryFact();
|
||||||
const [clearDialogOpen, setClearDialogOpen] = useState(false);
|
const [clearDialogOpen, setClearDialogOpen] = useState(false);
|
||||||
const [factToDelete, setFactToDelete] = useState<MemoryFact | null>(null);
|
const [factToDelete, setFactToDelete] = useState<MemoryFact | null>(null);
|
||||||
@ -222,6 +291,10 @@ export function MemorySettingsPage() {
|
|||||||
);
|
);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [filter, setFilter] = useState<MemoryViewFilter>("all");
|
const [filter, setFilter] = useState<MemoryViewFilter>("all");
|
||||||
|
const [pendingImport, setPendingImport] = useState<PendingImport | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
const deferredQuery = useDeferredValue(query);
|
const deferredQuery = useDeferredValue(query);
|
||||||
const normalizedQuery = deferredQuery.trim().toLowerCase();
|
const normalizedQuery = deferredQuery.trim().toLowerCase();
|
||||||
const factContentInputId = useId();
|
const factContentInputId = useId();
|
||||||
@ -258,7 +331,6 @@ export function MemorySettingsPage() {
|
|||||||
const factSave = t.settings.memory.factSave;
|
const factSave = t.settings.memory.factSave;
|
||||||
const factValidationContent = t.settings.memory.factValidationContent;
|
const factValidationContent = t.settings.memory.factValidationContent;
|
||||||
const factValidationConfidence = t.settings.memory.factValidationConfidence;
|
const factValidationConfidence = t.settings.memory.factValidationConfidence;
|
||||||
const manualFactSource = t.settings.memory.manualFactSource;
|
|
||||||
const noFacts = t.settings.memory.noFacts ?? "No saved facts yet.";
|
const noFacts = t.settings.memory.noFacts ?? "No saved facts yet.";
|
||||||
const summaryReadOnly = t.settings.memory.summaryReadOnly;
|
const summaryReadOnly = t.settings.memory.summaryReadOnly;
|
||||||
const memoryFullyEmpty =
|
const memoryFullyEmpty =
|
||||||
@ -271,6 +343,11 @@ export function MemorySettingsPage() {
|
|||||||
const filterFacts = t.settings.memory.filterFacts ?? "Facts";
|
const filterFacts = t.settings.memory.filterFacts ?? "Facts";
|
||||||
const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries";
|
const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries";
|
||||||
const noMatches = t.settings.memory.noMatches ?? "No matching memory found";
|
const noMatches = t.settings.memory.noMatches ?? "No matching memory found";
|
||||||
|
const exportButton = t.settings.memory.exportButton ?? t.common.export;
|
||||||
|
const exportSuccess =
|
||||||
|
t.settings.memory.exportSuccess ?? t.common.exportSuccess;
|
||||||
|
const importButton = t.settings.memory.importButton ?? t.common.import;
|
||||||
|
const importSuccess = t.settings.memory.importSuccess ?? "Memory imported";
|
||||||
|
|
||||||
const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : [];
|
const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : [];
|
||||||
const filteredSectionGroups = sectionGroups
|
const filteredSectionGroups = sectionGroups
|
||||||
@ -308,6 +385,68 @@ export function MemorySettingsPage() {
|
|||||||
(showSummaries && filteredSectionGroups.length > 0) ||
|
(showSummaries && filteredSectionGroups.length > 0) ||
|
||||||
(showFacts && filteredFacts.length > 0);
|
(showFacts && filteredFacts.length > 0);
|
||||||
|
|
||||||
|
async function handleExportMemory() {
|
||||||
|
try {
|
||||||
|
setIsExporting(true);
|
||||||
|
const exportedMemory = await exportMemory();
|
||||||
|
const fileName = `deerflow-memory-${(exportedMemory.lastUpdated || new Date().toISOString()).replace(/[:.]/g, "-")}.json`;
|
||||||
|
const blob = new Blob([JSON.stringify(exportedMemory, null, 2)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
toast.success(exportSuccess);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setIsExporting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportFileSelection(event: {
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}) {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
event.target.value = "";
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(await file.text());
|
||||||
|
if (!isImportedMemory(parsed)) {
|
||||||
|
toast.error(t.settings.memory.importInvalidFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPendingImport({
|
||||||
|
fileName: file.name,
|
||||||
|
memory: parsed,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
toast.error(t.settings.memory.importInvalidFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmImport() {
|
||||||
|
if (!pendingImport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await importMemoryMutation.mutateAsync(pendingImport.memory);
|
||||||
|
toast.success(importSuccess);
|
||||||
|
setPendingImport(null);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleClearMemory() {
|
async function handleClearMemory() {
|
||||||
try {
|
try {
|
||||||
await clearMemory.mutateAsync();
|
await clearMemory.mutateAsync();
|
||||||
@ -416,7 +555,7 @@ export function MemorySettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||||
<div className="flex flex-1 flex-col gap-3 sm:flex-row sm:items-center">
|
<div className="flex flex-1 flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
<Input
|
<Input
|
||||||
value={query}
|
value={query}
|
||||||
@ -440,7 +579,30 @@ export function MemorySettingsPage() {
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json,application/json"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(event) => void handleImportFileSelection(event)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={importMemoryMutation.isPending}
|
||||||
|
>
|
||||||
|
<UploadIcon className="mr-2 h-4 w-4" />
|
||||||
|
{importButton}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => void handleExportMemory()}
|
||||||
|
disabled={isExporting}
|
||||||
|
>
|
||||||
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
|
{isExporting ? t.common.loading : exportButton}
|
||||||
|
</Button>
|
||||||
<Button variant="outline" onClick={openCreateFactDialog}>
|
<Button variant="outline" onClick={openCreateFactDialog}>
|
||||||
<PlusIcon className="mr-2 h-4 w-4" />
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
{addFactLabel}
|
{addFactLabel}
|
||||||
@ -519,22 +681,25 @@ export function MemorySettingsPage() {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
{formatTimeAgo(fact.createdAt)}
|
{formatTimeAgo(fact.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t.settings.memory.markdown.table.source}:
|
||||||
|
</span>{" "}
|
||||||
|
{fact.source === "manual" ? (
|
||||||
|
t.settings.memory.manualFactSource
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={pathOfThread(fact.source)}
|
||||||
|
className="text-primary underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
{t.settings.memory.markdown.table.view}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm break-words">
|
<p className="text-sm break-words">
|
||||||
{fact.content}
|
{fact.content}
|
||||||
</p>
|
</p>
|
||||||
{fact.source === "manual" ? (
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
{manualFactSource}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href={pathOfThread(fact.source)}
|
|
||||||
className="text-primary text-sm underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
{t.settings.memory.markdown.table.view}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-1 self-start sm:ml-3">
|
<div className="flex shrink-0 items-center gap-1 self-start sm:ml-3">
|
||||||
@ -753,6 +918,65 @@ export function MemorySettingsPage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={pendingImport !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setPendingImport(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t.settings.memory.importConfirmTitle}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t.settings.memory.importConfirmDescription}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{pendingImport ? (
|
||||||
|
<div className="bg-muted rounded-md border p-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t.settings.memory.importFileLabel}:
|
||||||
|
</span>{" "}
|
||||||
|
{pendingImport.fileName}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t.settings.memory.markdown.facts}:
|
||||||
|
</span>{" "}
|
||||||
|
{pendingImport.memory.facts.length}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t.common.lastUpdated}:
|
||||||
|
</span>{" "}
|
||||||
|
{pendingImport.memory.lastUpdated
|
||||||
|
? formatTimeAgo(pendingImport.memory.lastUpdated)
|
||||||
|
: "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setPendingImport(null)}
|
||||||
|
disabled={importMemoryMutation.isPending}
|
||||||
|
>
|
||||||
|
{t.common.cancel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => void handleConfirmImport()}
|
||||||
|
disabled={importMemoryMutation.isPending}
|
||||||
|
>
|
||||||
|
{importMemoryMutation.isPending
|
||||||
|
? t.common.loading
|
||||||
|
: t.common.import}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const enUS: Translations = {
|
|||||||
save: "Save",
|
save: "Save",
|
||||||
install: "Install",
|
install: "Install",
|
||||||
create: "Create",
|
create: "Create",
|
||||||
|
import: "Import",
|
||||||
export: "Export",
|
export: "Export",
|
||||||
exportAsMarkdown: "Export as Markdown",
|
exportAsMarkdown: "Export as Markdown",
|
||||||
exportAsJSON: "Export as JSON",
|
exportAsJSON: "Export as JSON",
|
||||||
@ -313,6 +314,17 @@ export const enUS: Translations = {
|
|||||||
"DeerFlow automatically learns from your conversations in the background. These memories help DeerFlow understand you better and deliver a more personalized experience.",
|
"DeerFlow automatically learns from your conversations in the background. These memories help DeerFlow understand you better and deliver a more personalized experience.",
|
||||||
empty: "No memory data to display.",
|
empty: "No memory data to display.",
|
||||||
rawJson: "Raw JSON",
|
rawJson: "Raw JSON",
|
||||||
|
exportButton: "Export memory",
|
||||||
|
exportSuccess: "Memory exported",
|
||||||
|
importButton: "Import memory",
|
||||||
|
importConfirmTitle: "Import memory?",
|
||||||
|
importConfirmDescription:
|
||||||
|
"This will overwrite your current memory with the selected JSON backup.",
|
||||||
|
importFileLabel: "Selected file",
|
||||||
|
importInvalidFile:
|
||||||
|
"Failed to read the selected memory file. Please choose a valid JSON export.",
|
||||||
|
importSuccess: "Memory imported",
|
||||||
|
manualFactSource: "Manual",
|
||||||
addFact: "Add fact",
|
addFact: "Add fact",
|
||||||
addFactTitle: "Add memory fact",
|
addFactTitle: "Add memory fact",
|
||||||
editFactTitle: "Edit memory fact",
|
editFactTitle: "Edit memory fact",
|
||||||
@ -336,7 +348,6 @@ export const enUS: Translations = {
|
|||||||
factSave: "Save fact",
|
factSave: "Save fact",
|
||||||
factValidationContent: "Fact content cannot be empty.",
|
factValidationContent: "Fact content cannot be empty.",
|
||||||
factValidationConfidence: "Confidence must be a number between 0 and 1.",
|
factValidationConfidence: "Confidence must be a number between 0 and 1.",
|
||||||
manualFactSource: "Manual",
|
|
||||||
noFacts: "No saved facts yet.",
|
noFacts: "No saved facts yet.",
|
||||||
summaryReadOnly:
|
summaryReadOnly:
|
||||||
"Summary sections are read-only for now. You can currently add, edit, or delete individual facts, or clear all memory.",
|
"Summary sections are read-only for now. You can currently add, edit, or delete individual facts, or clear all memory.",
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export interface Translations {
|
|||||||
save: string;
|
save: string;
|
||||||
install: string;
|
install: string;
|
||||||
create: string;
|
create: string;
|
||||||
|
import: string;
|
||||||
export: string;
|
export: string;
|
||||||
exportAsMarkdown: string;
|
exportAsMarkdown: string;
|
||||||
exportAsJSON: string;
|
exportAsJSON: string;
|
||||||
@ -248,6 +249,15 @@ export interface Translations {
|
|||||||
description: string;
|
description: string;
|
||||||
empty: string;
|
empty: string;
|
||||||
rawJson: string;
|
rawJson: string;
|
||||||
|
exportButton: string;
|
||||||
|
exportSuccess: string;
|
||||||
|
importButton: string;
|
||||||
|
importConfirmTitle: string;
|
||||||
|
importConfirmDescription: string;
|
||||||
|
importFileLabel: string;
|
||||||
|
importInvalidFile: string;
|
||||||
|
importSuccess: string;
|
||||||
|
manualFactSource: string;
|
||||||
addFact: string;
|
addFact: string;
|
||||||
addFactTitle: string;
|
addFactTitle: string;
|
||||||
editFactTitle: string;
|
editFactTitle: string;
|
||||||
@ -269,7 +279,6 @@ export interface Translations {
|
|||||||
factSave: string;
|
factSave: string;
|
||||||
factValidationContent: string;
|
factValidationContent: string;
|
||||||
factValidationConfidence: string;
|
factValidationConfidence: string;
|
||||||
manualFactSource: string;
|
|
||||||
noFacts: string;
|
noFacts: string;
|
||||||
summaryReadOnly: string;
|
summaryReadOnly: string;
|
||||||
memoryFullyEmpty: string;
|
memoryFullyEmpty: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
CompassIcon,
|
CompassIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
@ -44,6 +44,7 @@ export const zhCN: Translations = {
|
|||||||
save: "保存",
|
save: "保存",
|
||||||
install: "安装",
|
install: "安装",
|
||||||
create: "创建",
|
create: "创建",
|
||||||
|
import: "导入",
|
||||||
export: "导出",
|
export: "导出",
|
||||||
exportAsMarkdown: "导出为 Markdown",
|
exportAsMarkdown: "导出为 Markdown",
|
||||||
exportAsJSON: "导出为 JSON",
|
exportAsJSON: "导出为 JSON",
|
||||||
@ -299,6 +300,15 @@ export const zhCN: Translations = {
|
|||||||
"DeerFlow 会在后台不断从你的对话中自动学习。这些记忆能帮助 DeerFlow 更好地理解你,并提供更个性化的体验。",
|
"DeerFlow 会在后台不断从你的对话中自动学习。这些记忆能帮助 DeerFlow 更好地理解你,并提供更个性化的体验。",
|
||||||
empty: "暂无可展示的记忆数据。",
|
empty: "暂无可展示的记忆数据。",
|
||||||
rawJson: "原始 JSON",
|
rawJson: "原始 JSON",
|
||||||
|
exportButton: "导出记忆",
|
||||||
|
exportSuccess: "记忆已导出",
|
||||||
|
importButton: "导入记忆",
|
||||||
|
importConfirmTitle: "导入记忆?",
|
||||||
|
importConfirmDescription: "这会用选中的 JSON 备份覆盖当前记忆。",
|
||||||
|
importFileLabel: "已选择文件",
|
||||||
|
importInvalidFile: "读取记忆文件失败,请选择有效的 JSON 导出文件。",
|
||||||
|
importSuccess: "记忆已导入",
|
||||||
|
manualFactSource: "手动添加",
|
||||||
addFact: "添加事实",
|
addFact: "添加事实",
|
||||||
addFactTitle: "添加记忆事实",
|
addFactTitle: "添加记忆事实",
|
||||||
editFactTitle: "编辑记忆事实",
|
editFactTitle: "编辑记忆事实",
|
||||||
@ -322,10 +332,9 @@ export const zhCN: Translations = {
|
|||||||
factSave: "保存事实",
|
factSave: "保存事实",
|
||||||
factValidationContent: "事实内容不能为空。",
|
factValidationContent: "事实内容不能为空。",
|
||||||
factValidationConfidence: "置信度必须是 0 到 1 之间的数字。",
|
factValidationConfidence: "置信度必须是 0 到 1 之间的数字。",
|
||||||
manualFactSource: "手动添加",
|
|
||||||
noFacts: "还没有保存的事实。",
|
noFacts: "还没有保存的事实。",
|
||||||
summaryReadOnly:
|
summaryReadOnly:
|
||||||
"摘要分区当前仍为只读。你可以在下方添加、编辑或删除事实,或清空全部记忆。",
|
"摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
|
||||||
memoryFullyEmpty: "还没有保存任何记忆。",
|
memoryFullyEmpty: "还没有保存任何记忆。",
|
||||||
factPreviewLabel: "即将删除的事实",
|
factPreviewLabel: "即将删除的事实",
|
||||||
searchPlaceholder: "搜索记忆",
|
searchPlaceholder: "搜索记忆",
|
||||||
|
|||||||
@ -10,12 +10,69 @@ async function readMemoryResponse(
|
|||||||
response: Response,
|
response: Response,
|
||||||
fallbackMessage: string,
|
fallbackMessage: string,
|
||||||
): Promise<UserMemory> {
|
): Promise<UserMemory> {
|
||||||
|
function formatErrorDetail(detail: unknown): string | null {
|
||||||
|
if (typeof detail === "string") {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(detail)) {
|
||||||
|
const parts = detail
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item && typeof item === "object") {
|
||||||
|
const record = item as Record<string, unknown>;
|
||||||
|
if (typeof record.msg === "string") {
|
||||||
|
return record.msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(record);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(item);
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join("; ") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail && typeof detail === "object") {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(detail);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof detail === "string" ||
|
||||||
|
typeof detail === "number" ||
|
||||||
|
typeof detail === "boolean" ||
|
||||||
|
typeof detail === "bigint"
|
||||||
|
) {
|
||||||
|
return String(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof detail === "symbol") {
|
||||||
|
return detail.description ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = (await response.json().catch(() => ({}))) as {
|
const errorData = (await response.json().catch(() => ({}))) as {
|
||||||
detail?: string;
|
detail?: unknown;
|
||||||
};
|
};
|
||||||
|
const detailMessage = formatErrorDetail(errorData.detail);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
errorData.detail ?? `${fallbackMessage}: ${response.statusText}`,
|
detailMessage ?? `${fallbackMessage}: ${response.statusText}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +101,22 @@ export async function deleteMemoryFact(factId: string): Promise<UserMemory> {
|
|||||||
return readMemoryResponse(response, "Failed to delete memory fact");
|
return readMemoryResponse(response, "Failed to delete memory fact");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function exportMemory(): Promise<UserMemory> {
|
||||||
|
const response = await fetch(`${getBackendBaseURL()}/api/memory/export`);
|
||||||
|
return readMemoryResponse(response, "Failed to export memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importMemory(memory: UserMemory): Promise<UserMemory> {
|
||||||
|
const response = await fetch(`${getBackendBaseURL()}/api/memory/import`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(memory),
|
||||||
|
});
|
||||||
|
return readMemoryResponse(response, "Failed to import memory");
|
||||||
|
}
|
||||||
|
|
||||||
export async function createMemoryFact(
|
export async function createMemoryFact(
|
||||||
input: MemoryFactInput,
|
input: MemoryFactInput,
|
||||||
): Promise<UserMemory> {
|
): Promise<UserMemory> {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
clearMemory,
|
clearMemory,
|
||||||
createMemoryFact,
|
createMemoryFact,
|
||||||
deleteMemoryFact,
|
deleteMemoryFact,
|
||||||
|
importMemory,
|
||||||
loadMemory,
|
loadMemory,
|
||||||
updateMemoryFact,
|
updateMemoryFact,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
@ -43,6 +44,17 @@ export function useDeleteMemoryFact() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useImportMemory() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (memory: UserMemory) => importMemory(memory),
|
||||||
|
onSuccess: (memory) => {
|
||||||
|
queryClient.setQueryData<UserMemory>(["memory"], memory);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useCreateMemoryFact() {
|
export function useCreateMemoryFact() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user