2026-02-03 13:41:04 +08:00
|
|
|
"""Memory API router for retrieving and managing global memory data."""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
refactor: split backend into harness (deerflow.*) and app (app.*) (#1131)
* refactor: extract shared utils to break harness→app cross-layer imports
Move _validate_skill_frontmatter to src/skills/validation.py and
CONVERTIBLE_EXTENSIONS + convert_file_to_markdown to src/utils/file_conversion.py.
This eliminates the two reverse dependencies from client.py (harness layer)
into gateway/routers/ (app layer), preparing for the harness/app package split.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: split backend/src into harness (deerflow.*) and app (app.*)
Physically split the monolithic backend/src/ package into two layers:
- **Harness** (`packages/harness/deerflow/`): publishable agent framework
package with import prefix `deerflow.*`. Contains agents, sandbox, tools,
models, MCP, skills, config, and all core infrastructure.
- **App** (`app/`): unpublished application code with import prefix `app.*`.
Contains gateway (FastAPI REST API) and channels (IM integrations).
Key changes:
- Move 13 harness modules to packages/harness/deerflow/ via git mv
- Move gateway + channels to app/ via git mv
- Rename all imports: src.* → deerflow.* (harness) / app.* (app layer)
- Set up uv workspace with deerflow-harness as workspace member
- Update langgraph.json, config.example.yaml, all scripts, Docker files
- Add build-system (hatchling) to harness pyproject.toml
- Add PYTHONPATH=. to gateway startup commands for app.* resolution
- Update ruff.toml with known-first-party for import sorting
- Update all documentation to reflect new directory structure
Boundary rule enforced: harness code never imports from app.
All 429 tests pass. Lint clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add harness→app boundary check test and update docs
Add test_harness_boundary.py that scans all Python files in
packages/harness/deerflow/ and fails if any `from app.*` or
`import app.*` statement is found. This enforces the architectural
rule that the harness layer never depends on the app layer.
Update CLAUDE.md to document the harness/app split architecture,
import conventions, and the boundary enforcement test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add config versioning with auto-upgrade on startup
When config.example.yaml schema changes, developers' local config.yaml
files can silently become outdated. This adds a config_version field and
auto-upgrade mechanism so breaking changes (like src.* → deerflow.*
renames) are applied automatically before services start.
- Add config_version: 1 to config.example.yaml
- Add startup version check warning in AppConfig.from_file()
- Add scripts/config-upgrade.sh with migration registry for value replacements
- Add `make config-upgrade` target
- Auto-run config-upgrade in serve.sh and start-daemon.sh before starting services
- Add config error hints in service failure messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix comments
* fix: update src.* import in test_sandbox_tools_security to deerflow.*
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle empty config and search parent dirs for config.example.yaml
Address Copilot review comments on PR #1131:
- Guard against yaml.safe_load() returning None for empty config files
- Search parent directories for config.example.yaml instead of only
looking next to config.yaml, fixing detection in common setups
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct skills root path depth and config_version type coercion
- loader.py: fix get_skills_root_path() to use 5 parent levels (was 3)
after harness split, file lives at packages/harness/deerflow/skills/
so parent×3 resolved to backend/packages/harness/ instead of backend/
- app_config.py: coerce config_version to int() before comparison in
_check_config_version() to prevent TypeError when YAML stores value
as string (e.g. config_version: "1")
- tests: add regression tests for both fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: update test imports from src.* to deerflow.*/app.* after harness refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:55:52 +08:00
|
|
|
from deerflow.agents.memory.updater import get_memory_data, reload_memory_data
|
|
|
|
|
from deerflow.config.memory_config import get_memory_config
|
2026-02-03 13:41:04 +08:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api", tags=["memory"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ContextSection(BaseModel):
|
|
|
|
|
"""Model for context sections (user and history)."""
|
|
|
|
|
|
|
|
|
|
summary: str = Field(default="", description="Summary content")
|
|
|
|
|
updatedAt: str = Field(default="", description="Last update timestamp")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UserContext(BaseModel):
|
|
|
|
|
"""Model for user context."""
|
|
|
|
|
|
|
|
|
|
workContext: ContextSection = Field(default_factory=ContextSection)
|
|
|
|
|
personalContext: ContextSection = Field(default_factory=ContextSection)
|
|
|
|
|
topOfMind: ContextSection = Field(default_factory=ContextSection)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HistoryContext(BaseModel):
|
|
|
|
|
"""Model for history context."""
|
|
|
|
|
|
|
|
|
|
recentMonths: ContextSection = Field(default_factory=ContextSection)
|
|
|
|
|
earlierContext: ContextSection = Field(default_factory=ContextSection)
|
|
|
|
|
longTermBackground: ContextSection = Field(default_factory=ContextSection)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Fact(BaseModel):
|
|
|
|
|
"""Model for a memory fact."""
|
|
|
|
|
|
|
|
|
|
id: str = Field(..., description="Unique identifier for the fact")
|
|
|
|
|
content: str = Field(..., description="Fact content")
|
|
|
|
|
category: str = Field(default="context", description="Fact category")
|
|
|
|
|
confidence: float = Field(default=0.5, description="Confidence score (0-1)")
|
|
|
|
|
createdAt: str = Field(default="", description="Creation timestamp")
|
|
|
|
|
source: str = Field(default="unknown", description="Source thread ID")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoryResponse(BaseModel):
|
|
|
|
|
"""Response model for memory data."""
|
|
|
|
|
|
|
|
|
|
version: str = Field(default="1.0", description="Memory schema version")
|
|
|
|
|
lastUpdated: str = Field(default="", description="Last update timestamp")
|
|
|
|
|
user: UserContext = Field(default_factory=UserContext)
|
|
|
|
|
history: HistoryContext = Field(default_factory=HistoryContext)
|
|
|
|
|
facts: list[Fact] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoryConfigResponse(BaseModel):
|
|
|
|
|
"""Response model for memory configuration."""
|
|
|
|
|
|
|
|
|
|
enabled: bool = Field(..., description="Whether memory is enabled")
|
|
|
|
|
storage_path: str = Field(..., description="Path to memory storage file")
|
|
|
|
|
debounce_seconds: int = Field(..., description="Debounce time for memory updates")
|
|
|
|
|
max_facts: int = Field(..., description="Maximum number of facts to store")
|
|
|
|
|
fact_confidence_threshold: float = Field(..., description="Minimum confidence threshold for facts")
|
|
|
|
|
injection_enabled: bool = Field(..., description="Whether memory injection is enabled")
|
|
|
|
|
max_injection_tokens: int = Field(..., description="Maximum tokens for memory injection")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoryStatusResponse(BaseModel):
|
|
|
|
|
"""Response model for memory status."""
|
|
|
|
|
|
|
|
|
|
config: MemoryConfigResponse
|
|
|
|
|
data: MemoryResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/memory",
|
|
|
|
|
response_model=MemoryResponse,
|
|
|
|
|
summary="Get Memory Data",
|
|
|
|
|
description="Retrieve the current global memory data including user context, history, and facts.",
|
|
|
|
|
)
|
|
|
|
|
async def get_memory() -> MemoryResponse:
|
|
|
|
|
"""Get the current global memory data.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The current memory data with user context, history, and facts.
|
|
|
|
|
|
|
|
|
|
Example Response:
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"version": "1.0",
|
|
|
|
|
"lastUpdated": "2024-01-15T10:30:00Z",
|
|
|
|
|
"user": {
|
|
|
|
|
"workContext": {"summary": "Working on DeerFlow project", "updatedAt": "..."},
|
|
|
|
|
"personalContext": {"summary": "Prefers concise responses", "updatedAt": "..."},
|
|
|
|
|
"topOfMind": {"summary": "Building memory API", "updatedAt": "..."}
|
|
|
|
|
},
|
|
|
|
|
"history": {
|
|
|
|
|
"recentMonths": {"summary": "Recent development activities", "updatedAt": "..."},
|
|
|
|
|
"earlierContext": {"summary": "", "updatedAt": ""},
|
|
|
|
|
"longTermBackground": {"summary": "", "updatedAt": ""}
|
|
|
|
|
},
|
|
|
|
|
"facts": [
|
|
|
|
|
{
|
|
|
|
|
"id": "fact_abc123",
|
|
|
|
|
"content": "User prefers TypeScript over JavaScript",
|
|
|
|
|
"category": "preference",
|
|
|
|
|
"confidence": 0.9,
|
|
|
|
|
"createdAt": "2024-01-15T10:30:00Z",
|
|
|
|
|
"source": "thread_xyz"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
"""
|
|
|
|
|
memory_data = get_memory_data()
|
|
|
|
|
return MemoryResponse(**memory_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/memory/reload",
|
|
|
|
|
response_model=MemoryResponse,
|
|
|
|
|
summary="Reload Memory Data",
|
|
|
|
|
description="Reload memory data from the storage file, refreshing the in-memory cache.",
|
|
|
|
|
)
|
|
|
|
|
async def reload_memory() -> MemoryResponse:
|
|
|
|
|
"""Reload memory data from file.
|
|
|
|
|
|
|
|
|
|
This forces a reload of the memory data from the storage file,
|
|
|
|
|
useful when the file has been modified externally.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The reloaded memory data.
|
|
|
|
|
"""
|
|
|
|
|
memory_data = reload_memory_data()
|
|
|
|
|
return MemoryResponse(**memory_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/memory/config",
|
|
|
|
|
response_model=MemoryConfigResponse,
|
|
|
|
|
summary="Get Memory Configuration",
|
|
|
|
|
description="Retrieve the current memory system configuration.",
|
|
|
|
|
)
|
|
|
|
|
async def get_memory_config_endpoint() -> MemoryConfigResponse:
|
|
|
|
|
"""Get the memory system configuration.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The current memory configuration settings.
|
|
|
|
|
|
|
|
|
|
Example Response:
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"enabled": true,
|
|
|
|
|
"storage_path": ".deer-flow/memory.json",
|
|
|
|
|
"debounce_seconds": 30,
|
|
|
|
|
"max_facts": 100,
|
|
|
|
|
"fact_confidence_threshold": 0.7,
|
|
|
|
|
"injection_enabled": true,
|
|
|
|
|
"max_injection_tokens": 2000
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
"""
|
|
|
|
|
config = get_memory_config()
|
|
|
|
|
return MemoryConfigResponse(
|
|
|
|
|
enabled=config.enabled,
|
|
|
|
|
storage_path=config.storage_path,
|
|
|
|
|
debounce_seconds=config.debounce_seconds,
|
|
|
|
|
max_facts=config.max_facts,
|
|
|
|
|
fact_confidence_threshold=config.fact_confidence_threshold,
|
|
|
|
|
injection_enabled=config.injection_enabled,
|
|
|
|
|
max_injection_tokens=config.max_injection_tokens,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/memory/status",
|
|
|
|
|
response_model=MemoryStatusResponse,
|
|
|
|
|
summary="Get Memory Status",
|
|
|
|
|
description="Retrieve both memory configuration and current data in a single request.",
|
|
|
|
|
)
|
|
|
|
|
async def get_memory_status() -> MemoryStatusResponse:
|
|
|
|
|
"""Get the memory system status including configuration and data.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Combined memory configuration and current data.
|
|
|
|
|
"""
|
|
|
|
|
config = get_memory_config()
|
|
|
|
|
memory_data = get_memory_data()
|
|
|
|
|
|
|
|
|
|
return MemoryStatusResponse(
|
|
|
|
|
config=MemoryConfigResponse(
|
|
|
|
|
enabled=config.enabled,
|
|
|
|
|
storage_path=config.storage_path,
|
|
|
|
|
debounce_seconds=config.debounce_seconds,
|
|
|
|
|
max_facts=config.max_facts,
|
|
|
|
|
fact_confidence_threshold=config.fact_confidence_threshold,
|
|
|
|
|
injection_enabled=config.injection_enabled,
|
|
|
|
|
max_injection_tokens=config.max_injection_tokens,
|
|
|
|
|
),
|
|
|
|
|
data=MemoryResponse(**memory_data),
|
|
|
|
|
)
|