Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/git-vibe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ ai:
required: true
tools:
- recall_memory
- submit_recall_feedback
- assemble_context
- get_memory
- get_fact
Expand All @@ -165,6 +166,7 @@ ai:
required: true
tools:
- recall_memory
- submit_recall_feedback
- assemble_context
- get_memory
- get_fact
Expand All @@ -179,6 +181,7 @@ ai:
required: true
tools:
- recall_memory
- submit_recall_feedback
- assemble_context
- get_memory
- get_fact
Expand All @@ -193,10 +196,11 @@ ai:
required: true
tools:
- recall_memory
- submit_recall_feedback
- assemble_context
- get_memory
- get_fact
- keyword_search
- semantic_search
- list_communities
- get_community_summary
- get_community_summary
40 changes: 0 additions & 40 deletions .github/workflows/snyk.yml

This file was deleted.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ scrape job when Prometheus is shared.

Online recall-quality cards use `densemem_recall_feedback_total` and
`densemem_recall_feedback_quality_score`. They stay at zero until
`RECALL_FEEDBACK_ENABLED=true` registers the `submit_recall_feedback` tool and a
host LLM submits compact feedback for `recall_memory` results. Normal production
recall traffic still contributes request volume, result count, and latency.
recall feedback is enabled from the control portal config panel and a host LLM
submits compact feedback for `recall_memory` results. Normal production recall
traffic still contributes request volume, result count, and latency.

For the disposable demo image, keep the control portal disabled and use the
demo telemetry overlay instead:
Expand Down
49 changes: 47 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
</p>

<p align="center">
<a href="https://zenodo.org/records/20469578"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.20469578.svg" alt="DOI: 10.5281/zenodo.20469578" /></a>
<a href="https://zenodo.org/records/20519039"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.20519039.svg" alt="DOI: 10.5281/zenodo.20519039" /></a>
</p>

Dense-Mem 是一层给 MCP 客户端使用的持久记忆服务。它把来源、claims、facts、
Expand All @@ -50,7 +50,7 @@ Dense-Mem 负责把记忆状态存稳、管住,并返回可以解释给用户

Dense-Mem 也是这篇研究预印本的一部分:
[Governed Enterprise AI Memory Beyond RAG: From Vector Retrieval to Permissioned
Knowledge Graphs](https://zenodo.org/records/20469578)。
Knowledge Graphs](https://zenodo.org/records/20519039)。

## 项目介绍视频

Expand Down Expand Up @@ -123,6 +123,51 @@ URL、model 和 dimensions 提供 OpenAI 默认值:`https://api.openai.com/v1`
`text-embedding-3-small`、`1536`。因此最小本地部署只需要补上 `AI_API_KEY`。
如果切换到其他 embedding provider 或 model,请一起覆盖这些配置。

### Telemetry Overlay

Prometheus telemetry 默认关闭,是可选功能。要为 `/ui` 应用和 control portal
dashboards 收集 usage、performance、verifier token、embedding token、recall
和 promotion metrics,可以把基础 stack 和 telemetry overlay 一起启动:

```bash
curl -fsSLo prometheus.yml \
https://raw.githubusercontent.com/markhuangai/dense-mem/main/examples/prometheus.yml
curl -fsSLo docker-compose.telemetry.yml \
https://raw.githubusercontent.com/markhuangai/dense-mem/main/examples/docker-compose.telemetry.yml

export TELEMETRY_SCRAPE_TOKEN="$(openssl rand -hex 32)"
docker compose -f docker-compose.yml -f docker-compose.telemetry.yml up -d
```

这个 overlay 会在 `127.0.0.1:9090` 启动 Prometheus,保留 30 天样本,把
`TELEMETRY_SCRAPE_TOKEN` 作为 scrape secret 传给 Prometheus,并让 Dense-Mem
使用 `http://prometheus:9090` 查询 telemetry。它也会设置
`TELEMETRY_PROMETHEUS_JOB=dense-mem`,这样在共享 Prometheus 时 dashboard
只查询 `dense-mem` scrape job。

在线 recall quality 卡片使用 `densemem_recall_feedback_total` 和
`densemem_recall_feedback_quality_score`。在 control portal config panel
开启 recall feedback,并且宿主 LLM 为 `recall_memory` 结果提交 compact
feedback 之前,这些卡片会保持为零。正常生产 recall 流量仍然会贡献请求量、
结果数和延迟指标。

对于一次性 demo image,保持 control portal 关闭,改用 demo telemetry
overlay:

```bash
curl -fsSLo prometheus.demo.yml \
https://raw.githubusercontent.com/markhuangai/dense-mem/main/examples/prometheus.demo.yml
curl -fsSLo docker-compose.demo.telemetry.yml \
https://raw.githubusercontent.com/markhuangai/dense-mem/main/examples/docker-compose.demo.telemetry.yml

export TELEMETRY_SCRAPE_TOKEN="$(openssl rand -hex 32)"
docker compose -f docker-compose.yml -f docker-compose.demo.telemetry.yml up -d
```

demo overlay 会在私有 Compose network 上 scrape `demo:8091`,并设置
`TELEMETRY_PROMETHEUS_JOB=dense-mem-demo`。不要把这个 metrics listener
公开到公网。

## 能力对比

| 能力 | Dense-Mem | 文件记忆 | Vector DB | 通用 MCP memory |
Expand Down
17 changes: 9 additions & 8 deletions cmd/demo-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type scopedReaderAdapter struct {
inner neo4j.ScopedReader
}

const startupTimeout = 5 * time.Minute

func (a *scopedReaderAdapter) ScopedRead(ctx context.Context, profileID string, query string, params map[string]any) (any, []map[string]any, error) {
summary, rows, err := a.inner.ScopedRead(ctx, profileID, query, params)
return summary, rows, err
Expand Down Expand Up @@ -108,9 +110,8 @@ func main() {
validation.SetEmbeddingDimensions(cfg.GetEmbeddingDimensions())
middleware.SetAuthVerificationConcurrency(cfg.AuthVerifyMaxConcurrency)

// Create root context with timeout for startup. A cold database may need
// time to apply migrations before schema-dependent checks can run.
startupCtx, startupCancel := context.WithTimeout(context.Background(), 60*time.Second)
// A cold Neo4j instance can need several minutes to create schema indexes.
startupCtx, startupCancel := context.WithTimeout(context.Background(), startupTimeout)
defer startupCancel()

// Initialize Postgres connection (REQUIRED for production)
Expand Down Expand Up @@ -488,7 +489,7 @@ func main() {
FragmentList: fragmentListSvc,
Recall: recallRegistrySvc,
Metrics: discoverabilityMetrics,
RecallFeedbackEnabled: cfg.GetRecallFeedbackEnabled(),
RecallFeedbackConfig: appConfigService,
KeywordSearch: keywordSearchService,
SemanticSearch: semanticSearchService,
GraphQuery: graphQueryService,
Expand Down Expand Up @@ -554,10 +555,10 @@ func main() {
factRetractHandler := handler.NewFactRetractHandler(factRetractSvc)
communityReadHandler := handler.NewCommunityReadHandler(communityGetSvc)
communityListHandler := handler.NewCommunityListHandler(communityListSvc)
toolCatalogHandler := handler.NewToolCatalogHandler(toolRegistry)
toolReadHandler := handler.NewToolReadHandler(toolRegistry)
toolExecuteHandler := handler.NewToolExecuteHandler(toolRegistry)
mcpHandler := handler.NewMCPHandlerWithLifecycle(toolRegistry, logger, streamLifecycle)
toolCatalogHandler := handler.NewToolCatalogHandlerWithRuntimeConfig(toolRegistry, appConfigService)
toolReadHandler := handler.NewToolReadHandlerWithRuntimeConfig(toolRegistry, appConfigService)
toolExecuteHandler := handler.NewToolExecuteHandlerWithRuntimeConfig(toolRegistry, appConfigService)
mcpHandler := handler.NewMCPHandlerWithLifecycleAndRuntimeConfig(toolRegistry, logger, streamLifecycle, appConfigService)
openAPIAISafeHandler := handler.NewOpenAPIHandler(openAPIGen, openapi.SpecVariantAISafe)
openAPIFullHandler := handler.NewOpenAPIHandler(openAPIGen, openapi.SpecVariantFull)

Expand Down
5 changes: 3 additions & 2 deletions cmd/migrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/markhuangai/dense-mem/internal/storage/postgres"
)

const migrationTimeout = 5 * time.Minute

func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <up|down|status|backfill-neo4j>\n", os.Args[0])
Expand All @@ -36,8 +38,7 @@ func main() {
log.Fatalf("Failed to load config: %v", err)
}

// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), migrationTimeout)
defer cancel()

// Execute command
Expand Down
17 changes: 9 additions & 8 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type scopedReaderAdapter struct {
inner neo4j.ScopedReader
}

const startupTimeout = 5 * time.Minute

func (a *scopedReaderAdapter) ScopedRead(ctx context.Context, profileID string, query string, params map[string]any) (any, []map[string]any, error) {
summary, rows, err := a.inner.ScopedRead(ctx, profileID, query, params)
return summary, rows, err
Expand Down Expand Up @@ -101,9 +103,8 @@ func main() {
validation.SetEmbeddingDimensions(cfg.GetEmbeddingDimensions())
middleware.SetAuthVerificationConcurrency(cfg.AuthVerifyMaxConcurrency)

// Create root context with timeout for startup. A cold database may need
// time to apply migrations before schema-dependent checks can run.
startupCtx, startupCancel := context.WithTimeout(context.Background(), 60*time.Second)
// A cold Neo4j instance can need several minutes to create schema indexes.
startupCtx, startupCancel := context.WithTimeout(context.Background(), startupTimeout)
defer startupCancel()

// Initialize Postgres connection (REQUIRED for production)
Expand Down Expand Up @@ -436,7 +437,7 @@ func main() {
FragmentList: fragmentListSvc,
Recall: recallRegistrySvc,
Metrics: discoverabilityMetrics,
RecallFeedbackEnabled: cfg.GetRecallFeedbackEnabled(),
RecallFeedbackConfig: appConfigService,
KeywordSearch: keywordSearchService,
SemanticSearch: semanticSearchService,
GraphQuery: graphQueryService,
Expand Down Expand Up @@ -502,10 +503,10 @@ func main() {
factRetractHandler := handler.NewFactRetractHandler(factRetractSvc)
communityReadHandler := handler.NewCommunityReadHandler(communityGetSvc)
communityListHandler := handler.NewCommunityListHandler(communityListSvc)
toolCatalogHandler := handler.NewToolCatalogHandler(toolRegistry)
toolReadHandler := handler.NewToolReadHandler(toolRegistry)
toolExecuteHandler := handler.NewToolExecuteHandler(toolRegistry)
mcpHandler := handler.NewMCPHandlerWithLifecycle(toolRegistry, logger, streamLifecycle)
toolCatalogHandler := handler.NewToolCatalogHandlerWithRuntimeConfig(toolRegistry, appConfigService)
toolReadHandler := handler.NewToolReadHandlerWithRuntimeConfig(toolRegistry, appConfigService)
toolExecuteHandler := handler.NewToolExecuteHandlerWithRuntimeConfig(toolRegistry, appConfigService)
mcpHandler := handler.NewMCPHandlerWithLifecycleAndRuntimeConfig(toolRegistry, logger, streamLifecycle, appConfigService)
openAPIAISafeHandler := handler.NewOpenAPIHandler(openAPIGen, openapi.SpecVariantAISafe)
openAPIFullHandler := handler.NewOpenAPIHandler(openAPIGen, openapi.SpecVariantFull)

Expand Down
7 changes: 0 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ type Config struct {
ClaimWriteRateLimit int
ClaimReadRateLimit int
RecallValidatedClaimWeight float64
RecallFeedbackEnabled bool
PromoteTxTimeoutSeconds int
SkillPackImportHistoryDays int
AICommunityMaxNodes int
Expand Down Expand Up @@ -155,7 +154,6 @@ func (c *Config) GetAIVerifierMaxConcurrency() int { return c.AIVerifierMa
func (c *Config) GetClaimWriteRateLimit() int { return c.ClaimWriteRateLimit }
func (c *Config) GetClaimReadRateLimit() int { return c.ClaimReadRateLimit }
func (c *Config) GetRecallValidatedClaimWeight() float64 { return c.RecallValidatedClaimWeight }
func (c *Config) GetRecallFeedbackEnabled() bool { return c.RecallFeedbackEnabled }
func (c *Config) GetPromoteTxTimeoutSeconds() int { return c.PromoteTxTimeoutSeconds }
func (c *Config) GetSkillPackImportHistoryDays() int { return c.SkillPackImportHistoryDays }
func (c *Config) GetAICommunityMaxNodes() int { return c.AICommunityMaxNodes }
Expand Down Expand Up @@ -400,11 +398,6 @@ func Load() (Config, error) {
return cfg, err
}

cfg.RecallFeedbackEnabled, err = parseBoolOrDefault("RECALL_FEEDBACK_ENABLED", false)
if err != nil {
return cfg, err
}

cfg.PromoteTxTimeoutSeconds, err = parseIntOrDefault("PROMOTE_TX_TIMEOUT_SECONDS", 10)
if err != nil {
return cfg, err
Expand Down
19 changes: 0 additions & 19 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ func clearEnv() {
"CLAIM_WRITE_RATE_LIMIT",
"CLAIM_READ_RATE_LIMIT",
"RECALL_VALIDATED_CLAIM_WEIGHT",
"RECALL_FEEDBACK_ENABLED",
"PROMOTE_TX_TIMEOUT_SECONDS",
"AI_COMMUNITY_MAX_NODES",
"CONTROL_HTTP_ADDR",
Expand Down Expand Up @@ -134,9 +133,6 @@ func TestLoadDefaults(t *testing.T) {
if cfg.TelemetryPrometheusJob != "" {
t.Errorf("TelemetryPrometheusJob default = %q, want empty", cfg.TelemetryPrometheusJob)
}
if cfg.GetRecallFeedbackEnabled() {
t.Error("GetRecallFeedbackEnabled() default = true, want false")
}
}

func TestLoadTelemetryConfig(t *testing.T) {
Expand Down Expand Up @@ -193,21 +189,6 @@ func TestLoadTelemetryConfigRequiresScrapeToken(t *testing.T) {
}
}

func TestLoadRecallFeedbackConfig(t *testing.T) {
clearEnv()
setRequiredEnv()
os.Setenv("RECALL_FEEDBACK_ENABLED", "true")

cfg, err := Load()
if err != nil {
t.Fatalf("Load() returned unexpected error: %v", err)
}

if !cfg.GetRecallFeedbackEnabled() {
t.Fatal("GetRecallFeedbackEnabled() = false, want true")
}
}

func TestLoadValidation_MissingPostgresDSN(t *testing.T) {
clearEnv()
os.Setenv("NEO4J_URI", "bolt://localhost:7687")
Expand Down
23 changes: 23 additions & 0 deletions internal/domain/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const (
AppConfigCommunityDetectionJitterSeconds = "COMMUNITY_DETECTION_JITTER_SECONDS"

AppConfigOperationLogRetentionDays = "OPERATION_LOG_RETENTION_DAYS"

AppConfigRecallFeedbackEnabled = "RECALL_FEEDBACK_ENABLED"
)

type AppConfigEntry struct {
Expand Down Expand Up @@ -89,3 +91,24 @@ type OperationLogConfigItem struct {
type OperationLogRuntimeConfig struct {
RetentionDays int `json:"retention_days"`
}

// RecallFeedbackConfigSettings is the editable runtime configuration for
// host-LLM recall feedback telemetry.
type RecallFeedbackConfigSettings struct {
UpdateTime string `json:"update_time"`
Items []RecallFeedbackConfigItem `json:"items"`
Effective RecallFeedbackRuntimeConfig `json:"effective"`
}

// RecallFeedbackConfigItem is one control-panel editable recall feedback config entry.
type RecallFeedbackConfigItem struct {
Key string `json:"key"`
Value string `json:"value"`
EffectiveValue string `json:"effective_value"`
UpdatedAt time.Time `json:"updated_at"`
}

// RecallFeedbackRuntimeConfig is the effective recall feedback runtime config.
type RecallFeedbackRuntimeConfig struct {
Enabled bool `json:"enabled"`
}
2 changes: 2 additions & 0 deletions internal/http/control_portal.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ func NewControlPortalServerWithMetricsAndTelemetry(
api.PATCH("/config/community-detection", control.updateCommunityDetectionConfig)
api.GET("/config/operation-logs", control.getOperationLogConfig)
api.PATCH("/config/operation-logs", control.updateOperationLogConfig)
api.GET("/config/recall-feedback", control.getRecallFeedbackConfig)
api.PATCH("/config/recall-feedback", control.updateRecallFeedbackConfig)
}
if telemetry.SSO != nil {
api.GET("/sso/providers", control.listSSOProviders)
Expand Down
Loading
Loading