diff --git a/backend/README.md b/backend/README.md index 4c8e01c..e94debd 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,6 +2,7 @@ DeerFlow is a LangGraph-based AI agent system that provides a powerful "super agent" with sandbox execution capabilities. The backend enables AI agents to execute code, browse the web, manage files, and perform complex multi-step tasks in isolated environments. +--- ## Features - **LangGraph Agent Runtime**: Built on LangGraph for robust multi-agent workflow orchestration @@ -13,6 +14,7 @@ DeerFlow is a LangGraph-based AI agent system that provides a powerful "super ag - **Context Summarization**: Automatic conversation summarization for long conversations - **Plan Mode**: TodoList middleware for complex multi-step task tracking +--- ## Architecture ``` @@ -33,6 +35,7 @@ DeerFlow is a LangGraph-based AI agent system that provides a powerful "super ag - `/api/*` (other) → Gateway API (models, MCP, skills, artifacts, uploads) - `/` (non-API) → Frontend (web interface) +--- ## Quick Start ### Prerequisites @@ -101,6 +104,7 @@ Direct access: - LangGraph: http://localhost:2024 - Gateway: http://localhost:8001 +--- ## Project Structure ``` @@ -129,6 +133,7 @@ backend/ └── Dockerfile # Container build ``` +--- ## API Reference ### LangGraph API (via `/api/langgraph/*`) @@ -164,6 +169,7 @@ backend/ **Artifacts**: - `GET /api/threads/{thread_id}/artifacts/{path}` - Download generated artifacts +--- ## Configuration ### Main Configuration (`config.yaml`) @@ -209,6 +215,7 @@ MCP servers and skills are configured in `extensions_config.json`: - Model API keys: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `DEEPSEEK_API_KEY`, etc. - Tool API keys: `TAVILY_API_KEY`, `GITHUB_TOKEN`, etc. +--- ## Development ### Commands @@ -234,6 +241,7 @@ make format # Format code (ruff) uv run pytest ``` +--- ## Documentation - [Configuration Guide](docs/CONFIGURATION.md) - Detailed configuration options @@ -243,6 +251,7 @@ uv run pytest - [Summarization](docs/summarization.md) - Context summarization feature - [Plan Mode](docs/plan_mode_usage.md) - TodoList middleware usage +--- ## Technology Stack ### Core Frameworks @@ -266,10 +275,12 @@ uv run pytest - `firecrawl-py` - Web scraping - `ddgs` - DuckDuckGo image search +--- ## License See the [LICENSE](../LICENSE) file in the project root. +--- ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. diff --git a/frontend/src/components/landing/hero.tsx b/frontend/src/components/landing/hero.tsx index a9baa7c..2fde97b 100644 --- a/frontend/src/components/landing/hero.tsx +++ b/frontend/src/components/landing/hero.tsx @@ -58,11 +58,12 @@ export function Hero({ className }: { className?: string }) {

DeerFlow is an open-source SuperAgent that researches, codes, and + creates.
- creates. With the help of sandboxes, tools and skills, it handles + With the help of sandboxes, memories, tools and skills, it handles
different levels of tasks that could take minutes to hours.

diff --git a/frontend/src/components/landing/sections/whats-new-section.tsx b/frontend/src/components/landing/sections/whats-new-section.tsx index ac37029..cca4c98 100644 --- a/frontend/src/components/landing/sections/whats-new-section.tsx +++ b/frontend/src/components/landing/sections/whats-new-section.tsx @@ -11,12 +11,7 @@ const features: BentoCardProps[] = [ color: COLOR, label: "Context Engineering", title: "Long/Short-term Memory", - description: ( -
-
Now the agent can better understand you
-
Coming soon
-
- ), + description: "Now the agent can better understand you", }, { color: COLOR, diff --git a/frontend/src/components/workspace/settings/memory-settings-page.tsx b/frontend/src/components/workspace/settings/memory-settings-page.tsx new file mode 100644 index 0000000..860aef3 --- /dev/null +++ b/frontend/src/components/workspace/settings/memory-settings-page.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { Streamdown } from "streamdown"; + +import { useI18n } from "@/core/i18n/hooks"; +import { useMemory } from "@/core/memory/hooks"; +import type { UserMemory } from "@/core/memory/types"; +import { streamdownPlugins } from "@/core/streamdown/plugins"; +import { pathOfThread } from "@/core/threads/utils"; +import { formatTimeAgo } from "@/core/utils/datetime"; + +import { SettingsSection } from "./settings-section"; + +function confidenceToLevelKey(confidence: unknown): { + key: "veryHigh" | "high" | "normal" | "unknown"; + value?: number; +} { + if (typeof confidence !== "number" || !Number.isFinite(confidence)) { + return { key: "unknown" }; + } + + // Clamp to [0, 1] since confidence is expected to be a probability-like score. + const value = Math.min(1, Math.max(0, confidence)); + + // 3 levels: + // - veryHigh: [0.85, 1] + // - high: [0.65, 0.85) + // - normal: [0, 0.65) + if (value >= 0.85) return { key: "veryHigh", value }; + if (value >= 0.65) return { key: "high", value }; + return { key: "normal", value }; +} + +function memoryToMarkdown( + memory: UserMemory, + t: ReturnType["t"], +) { + const parts: string[] = []; + + console.info(memory); + + parts.push(`## ${t.settings.memory.markdown.overview}`); + parts.push(`- **${t.common.version}**: \`${memory.version}\``); + parts.push( + `- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``, + ); + + parts.push(`\n## ${t.settings.memory.markdown.userContext}`); + parts.push( + [ + `### ${t.settings.memory.markdown.work}`, + memory.user.workContext.summary || "-", + "", + memory.user.workContext.updatedAt && + `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.user.workContext.updatedAt)}\``, + ].join("\n"), + ); + parts.push( + [ + `### ${t.settings.memory.markdown.personal}`, + memory.user.personalContext.summary || "-", + "", + memory.user.personalContext.updatedAt && + `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.user.personalContext.updatedAt)}\``, + ].join("\n"), + ); + parts.push( + [ + `### ${t.settings.memory.markdown.topOfMind}`, + memory.user.topOfMind.summary || "-", + "", + memory.user.topOfMind.updatedAt && + `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.user.topOfMind.updatedAt)}\``, + ].join("\n"), + ); + + parts.push(`\n## ${t.settings.memory.markdown.historyBackground}`); + parts.push( + [ + `### ${t.settings.memory.markdown.recentMonths}`, + memory.history.recentMonths.summary || "-", + "", + memory.history.recentMonths.updatedAt && + `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.history.recentMonths.updatedAt)}\``, + ].join("\n"), + ); + parts.push( + [ + `### ${t.settings.memory.markdown.earlierContext}`, + memory.history.earlierContext.summary || "-", + "", + memory.history.earlierContext.updatedAt && + `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.history.earlierContext.updatedAt)}\``, + ].join("\n"), + ); + parts.push( + [ + `### ${t.settings.memory.markdown.longTermBackground}`, + memory.history.longTermBackground.summary || "-", + "", + memory.history.longTermBackground.updatedAt && + `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.history.longTermBackground.updatedAt)}\``, + ].join("\n"), + ); + + parts.push(`\n## ${t.settings.memory.markdown.facts}`); + if (memory.facts.length === 0) { + parts.push(`_${t.settings.memory.markdown.empty}_`); + } else { + parts.push( + [ + `| ${t.settings.memory.markdown.table.category} | ${t.settings.memory.markdown.table.confidence} | ${t.settings.memory.markdown.table.content} | ${t.settings.memory.markdown.table.source} | ${t.settings.memory.markdown.table.createdAt} |`, + "|---|---|---|---|---|", + ...memory.facts.map((f) => { + const { key, value } = confidenceToLevelKey(f.confidence); + const levelLabel = + t.settings.memory.markdown.table.confidenceLevel[key]; + const confidenceText = + typeof value === "number" ? `${levelLabel}` : levelLabel; + return `| ${upperFirst(f.category)} | ${confidenceText} | ${f.content} | [${t.settings.memory.markdown.table.view}](${pathOfThread(f.source)}) | ${formatTimeAgo(f.createdAt)} |`; + }), + ].join("\n"), + ); + } + + const markdown = parts.join("\n\n"); + + // Ensure every level-2 heading (##) is preceded by a horizontal rule. + const lines = markdown.split("\n"); + const out: string[] = []; + let i = 0; + for (const line of lines) { + i++; + if (i !== 1 && line.startsWith("## ")) { + if (out.length === 0 || out[out.length - 1] !== "---") { + out.push("---"); + } + } + out.push(line); + } + + return out.join("\n"); +} + +export function MemorySettingsPage() { + const { t } = useI18n(); + const { memory, isLoading, error } = useMemory(); + return ( + + {isLoading ? ( +
{t.common.loading}
+ ) : error ? ( +
Error: {error.message}
+ ) : !memory ? ( +
+ {t.settings.memory.empty} +
+ ) : ( +
+ + {memoryToMarkdown(memory, t)} + +
+ )} +
+ ); +} + +function upperFirst(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/frontend/src/components/workspace/settings/settings-dialog.tsx b/frontend/src/components/workspace/settings/settings-dialog.tsx index d3ede84..bd9f942 100644 --- a/frontend/src/components/workspace/settings/settings-dialog.tsx +++ b/frontend/src/components/workspace/settings/settings-dialog.tsx @@ -1,6 +1,12 @@ "use client"; -import { BellIcon, PaletteIcon, SparklesIcon, WrenchIcon } from "lucide-react"; +import { + BellIcon, + BrainIcon, + PaletteIcon, + SparklesIcon, + WrenchIcon, +} from "lucide-react"; import { useMemo, useState } from "react"; import { @@ -12,6 +18,7 @@ import { import { ScrollArea } from "@/components/ui/scroll-area"; import { AcknowledgePage } from "@/components/workspace/settings/acknowledge-page"; import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page"; +import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page"; import { NotificationSettingsPage } from "@/components/workspace/settings/notification-settings-page"; import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page"; import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page"; @@ -20,6 +27,7 @@ import { cn } from "@/lib/utils"; type SettingsSection = | "appearance" + | "memory" | "tools" | "skills" | "notification" @@ -29,11 +37,8 @@ type SettingsDialogProps = React.ComponentProps & { defaultSection?: SettingsSection; }; -export function SettingsDialog({ - defaultSection = "appearance", - onOpenChange, - ...dialogProps -}: SettingsDialogProps) { +export function SettingsDialog(props: SettingsDialogProps) { + const { defaultSection = "appearance", ...dialogProps } = props; const { t } = useI18n(); const [activeSection, setActiveSection] = useState(defaultSection); @@ -50,18 +55,27 @@ export function SettingsDialog({ label: t.settings.sections.notification, icon: BellIcon, }, + { + id: "memory", + label: t.settings.sections.memory, + icon: BrainIcon, + }, { id: "tools", label: t.settings.sections.tools, icon: WrenchIcon }, { id: "skills", label: t.settings.sections.skills, icon: SparklesIcon }, ], [ t.settings.sections.appearance, + t.settings.sections.memory, t.settings.sections.tools, t.settings.sections.skills, t.settings.sections.notification, ], ); return ( - + props.onOpenChange?.(open)} + >
{activeSection === "appearance" && } + {activeSection === "memory" && } {activeSection === "tools" && } {activeSection === "skills" && ( onOpenChange?.(false)} + onClose={() => props.onOpenChange?.(false)} /> )} {activeSection === "notification" && } diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index d055c5b..202c726 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -35,6 +35,8 @@ export const enUS: Translations = { custom: "Custom", notAvailableInDemoMode: "Not available in demo mode", loading: "Loading...", + version: "Version", + lastUpdated: "Last updated", code: "Code", preview: "Preview", cancel: "Cancel", @@ -201,11 +203,47 @@ export const enUS: Translations = { description: "Adjust how DeerFlow looks and behaves for you.", sections: { appearance: "Appearance", + memory: "Memory", tools: "Tools", skills: "Skills", notification: "Notification", acknowledge: "Acknowledge", }, + memory: { + title: "Memory", + description: + "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.", + rawJson: "Raw JSON", + markdown: { + overview: "Overview", + userContext: "User context", + work: "Work", + personal: "Personal", + topOfMind: "Top of mind", + historyBackground: "History", + recentMonths: "Recent months", + earlierContext: "Earlier context", + longTermBackground: "Long-term background", + updatedAt: "Updated at", + facts: "Facts", + empty: "Empty", + table: { + category: "Category", + confidence: "Confidence", + confidenceLevel: { + veryHigh: "Very high", + high: "High", + normal: "Normal", + unknown: "Unknown", + }, + content: "Content", + source: "Source", + createdAt: "CreatedAt", + view: "View", + }, + }, + }, appearance: { themeTitle: "Theme", themeDescription: diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 673e42f..aa83482 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -24,6 +24,8 @@ export interface Translations { custom: string; notAvailableInDemoMode: string; loading: string; + version: string; + lastUpdated: string; code: string; preview: string; cancel: string; @@ -149,11 +151,46 @@ export interface Translations { description: string; sections: { appearance: string; + memory: string; tools: string; skills: string; notification: string; acknowledge: string; }; + memory: { + title: string; + description: string; + empty: string; + rawJson: string; + markdown: { + overview: string; + userContext: string; + work: string; + personal: string; + topOfMind: string; + historyBackground: string; + recentMonths: string; + earlierContext: string; + longTermBackground: string; + updatedAt: string; + facts: string; + empty: string; + table: { + category: string; + confidence: string; + confidenceLevel: { + veryHigh: string; + high: string; + normal: string; + unknown: string; + }; + content: string; + source: string; + createdAt: string; + view: string; + }; + }; + }; appearance: { themeTitle: string; themeDescription: string; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index ae733bb..07847b6 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -35,6 +35,8 @@ export const zhCN: Translations = { custom: "自定义", notAvailableInDemoMode: "在演示模式下不可用", loading: "加载中...", + version: "版本", + lastUpdated: "最后更新", code: "代码", preview: "预览", cancel: "取消", @@ -197,11 +199,47 @@ export const zhCN: Translations = { description: "根据你的偏好调整 DeerFlow 的界面和行为。", sections: { appearance: "外观", + memory: "记忆", tools: "工具", skills: "技能", notification: "通知", acknowledge: "致谢", }, + memory: { + title: "记忆", + description: + "DeerFlow 会在后台不断从你的对话中自动学习。这些记忆能帮助 DeerFlow 更好地理解你,并提供更个性化的体验。", + empty: "暂无可展示的记忆数据。", + rawJson: "原始 JSON", + markdown: { + overview: "概览", + userContext: "用户上下文", + work: "工作", + personal: "个人", + topOfMind: "近期关注(Top of mind)", + historyBackground: "历史背景", + recentMonths: "近几个月", + earlierContext: "更早上下文", + longTermBackground: "长期背景", + updatedAt: "更新于", + facts: "事实", + empty: "(空)", + table: { + category: "类别", + confidence: "置信度", + confidenceLevel: { + veryHigh: "极高", + high: "较高", + normal: "一般", + unknown: "未知", + }, + content: "内容", + source: "来源", + createdAt: "创建时间", + view: "查看", + }, + }, + }, appearance: { themeTitle: "主题", themeDescription: "跟随系统或选择固定的界面模式。", diff --git a/frontend/src/core/memory/api.ts b/frontend/src/core/memory/api.ts new file mode 100644 index 0000000..b5d6f11 --- /dev/null +++ b/frontend/src/core/memory/api.ts @@ -0,0 +1,9 @@ +import { getBackendBaseURL } from "../config"; + +import type { UserMemory } from "./types"; + +export async function loadMemory() { + const memory = await fetch(`${getBackendBaseURL()}/api/memory`); + const json = await memory.json(); + return json as UserMemory; +} diff --git a/frontend/src/core/memory/hooks.ts b/frontend/src/core/memory/hooks.ts new file mode 100644 index 0000000..6e8454b --- /dev/null +++ b/frontend/src/core/memory/hooks.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; + +import { loadMemory } from "./api"; + +export function useMemory() { + const { data, isLoading, error } = useQuery({ + queryKey: ["memory"], + queryFn: () => loadMemory(), + }); + return { memory: data ?? null, isLoading, error }; +} diff --git a/frontend/src/core/memory/index.ts b/frontend/src/core/memory/index.ts new file mode 100644 index 0000000..1d58e1a --- /dev/null +++ b/frontend/src/core/memory/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./types"; diff --git a/frontend/src/core/memory/types.ts b/frontend/src/core/memory/types.ts new file mode 100644 index 0000000..2192937 --- /dev/null +++ b/frontend/src/core/memory/types.ts @@ -0,0 +1,40 @@ +export interface UserMemory { + version: string; + lastUpdated: string; + user: { + workContext: { + summary: string; + updatedAt: string; + }; + personalContext: { + summary: string; + updatedAt: string; + }; + topOfMind: { + summary: string; + updatedAt: string; + }; + }; + history: { + recentMonths: { + summary: string; + updatedAt: string; + }; + earlierContext: { + summary: string; + updatedAt: string; + }; + longTermBackground: { + summary: string; + updatedAt: string; + }; + }; + facts: { + id: string; + content: string; + category: string; + confidence: number; + createdAt: string; + source: string; + }[]; +} diff --git a/frontend/src/core/utils/datetime.ts b/frontend/src/core/utils/datetime.ts index 71bbd85..0d641af 100644 --- a/frontend/src/core/utils/datetime.ts +++ b/frontend/src/core/utils/datetime.ts @@ -1,7 +1,27 @@ import { formatDistanceToNow } from "date-fns"; +import { enUS as dateFnsEnUS, zhCN as dateFnsZhCN } from "date-fns/locale"; -export function formatTimeAgo(date: Date | string | number) { +import { detectLocale, type Locale } from "@/core/i18n"; +import { getLocaleFromCookie } from "@/core/i18n/cookies"; + +function getDateFnsLocale(locale: Locale) { + switch (locale) { + case "zh-CN": + return dateFnsZhCN; + case "en-US": + default: + return dateFnsEnUS; + } +} + +export function formatTimeAgo(date: Date | string | number, locale?: Locale) { + const effectiveLocale = + locale ?? + (getLocaleFromCookie() as Locale | null) ?? + // Fallback when cookie is missing (or on first render) + detectLocale(); return formatDistanceToNow(date, { addSuffix: true, + locale: getDateFnsLocale(effectiveLocale), }); }