Version: 0.9.23.dev.260416
后端: 1. Memory 管理面 API 落地(“我的记忆”增删改查 + 恢复) - 补齐 List/Get/Create/Update/Delete/Restore 的 handler、请求模型与返回视图 - 注册 `/api/v1/memory/items*` 路由并接入 MemoryHandler - 新增 memory item not found / invalid memory type / invalid memory content 三类管理面错误码 2. Memory Module / Service / Repo 扩展为“可管理 + 可治理”门面 - 新增 NewModuleWithObserve / ObserveDeps,导出 GetItem / CreateItem / UpdateItem / DeleteItem / RestoreItem / RunDedupCleanup / MemoryObserver / MemoryMetrics - 新增手动新增、修改、恢复能力;删除链路切到 SoftDeleteByID;所有管理动作统一事务内写 audit,并桥接向量同步与管理面观测 - 补齐 CreateItemFields / UpdateItemFields、单条 Create、管理侧字段更新、软删/恢复,以及 dedup 扫描/归档所需 repo 能力 - 审计操作补齐 archive / restore 3. Memory 读侧与注入侧观测补齐 - HybridRetrieve 返回 telemetry,统一记录 pinned hit / semantic hit / dedup drop / degraded / RAG fallback,并上报读取命中、去重丢弃、RAG 降级指标 - AgentService 持有 memory observer / metrics;injectMemoryContext 对读取失败、空注入、成功注入补齐结构化日志与注入计数 4. Worker / 决策 / 向量同步链路治理增强 - 召回结果显式携带 fallbackMode;hash 精确命中、rag→mysql 降级、最终动作统一写入决策观测 - 接入 vectorSyncer / observer / metrics;为 job 重试、任务成功/失败、决策分布与 fallback 补齐打点;向量 upsert/delete 统一改走公共 Syncer,并收敛 parseMemoryID 解析逻辑 5. 启动层接入 Memory 观测依赖 - 启动时创建 LoggerObserver + MetricsRegistry,并通过 NewModuleWithObserve 注入 memory 模块 前端:无 仓库:无
This commit is contained in:
@@ -33,6 +33,11 @@ type factDecisionResult struct {
|
||||
Outcomes []*ApplyActionOutcome
|
||||
}
|
||||
|
||||
type candidateRecallResult struct {
|
||||
Items []memorymodel.CandidateSnapshot
|
||||
FallbackMode string
|
||||
}
|
||||
|
||||
// executeDecisionFlow 在 worker 内编排"召回→逐对比对→汇总→执行"全流程。
|
||||
//
|
||||
// 职责边界:
|
||||
@@ -116,6 +121,7 @@ func (r *Runner) executeDecisionForFact(
|
||||
}
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
r.recordDecisionObservation(ctx, job, payload, fact, 0, memorymodel.DecisionActionNone, "hash_exact", true, nil)
|
||||
result.Outcomes = append(result.Outcomes, &ApplyActionOutcome{
|
||||
Action: memorymodel.DecisionActionNone,
|
||||
NeedsSync: false,
|
||||
@@ -124,7 +130,8 @@ func (r *Runner) executeDecisionForFact(
|
||||
}
|
||||
|
||||
// Step 2: Milvus 语义召回(含降级)。
|
||||
candidates := r.recallCandidates(ctx, payload, fact)
|
||||
recallResult := r.recallCandidates(ctx, payload, fact)
|
||||
candidates := recallResult.Items
|
||||
|
||||
// 打印召回候选详情,便于排查向量召回和阈值过滤效果。
|
||||
if r.logger != nil {
|
||||
@@ -151,9 +158,11 @@ func (r *Runner) executeDecisionForFact(
|
||||
// Step 5: 校验 + 执行。
|
||||
actionOutcome, err := ApplyFinalDecision(ctx, itemRepo, auditRepo, *decision, fact, job, payload)
|
||||
if err != nil {
|
||||
r.recordDecisionObservation(ctx, job, payload, fact, len(candidates), decision.Action, recallResult.FallbackMode, false, err)
|
||||
return nil, fmt.Errorf("执行决策动作失败: %w", err)
|
||||
}
|
||||
result.Outcomes = append(result.Outcomes, actionOutcome)
|
||||
r.recordDecisionObservation(ctx, job, payload, fact, len(candidates), decision.Action, recallResult.FallbackMode, true, nil)
|
||||
|
||||
// Step 6: conflict (DELETE) 后需要补一个 ADD 写入新 fact。
|
||||
// 原因:旧记忆矛盾需删除,但新事实本身仍然有效,必须写入。
|
||||
@@ -180,7 +189,7 @@ func (r *Runner) recallCandidates(
|
||||
ctx context.Context,
|
||||
payload memorymodel.ExtractJobPayload,
|
||||
fact memorymodel.NormalizedFact,
|
||||
) []memorymodel.CandidateSnapshot {
|
||||
) candidateRecallResult {
|
||||
// 1. 优先使用 Milvus 向量语义召回。
|
||||
if r.ragRuntime != nil {
|
||||
retrieveResult, err := r.ragRuntime.RetrieveMemory(ctx, infrarag.MemoryRetrieveRequest{
|
||||
@@ -194,7 +203,10 @@ func (r *Runner) recallCandidates(
|
||||
if err == nil && len(retrieveResult.Items) > 0 {
|
||||
candidates := r.buildCandidatesFromRAG(retrieveResult.Items)
|
||||
if len(candidates) > 0 {
|
||||
return candidates
|
||||
return candidateRecallResult{
|
||||
Items: candidates,
|
||||
FallbackMode: "rag",
|
||||
}
|
||||
}
|
||||
// RAG 返回了结果但 DocumentID 全部解析失败,降级到 MySQL。
|
||||
if r.logger != nil {
|
||||
@@ -204,10 +216,17 @@ func (r *Runner) recallCandidates(
|
||||
if err != nil && r.logger != nil {
|
||||
r.logger.Printf("[WARN][去重] Milvus 语义召回失败,降级到 MySQL: user_id=%d memory_type=%s topk=%d err=%v", payload.UserID, fact.MemoryType, r.cfg.DecisionCandidateTopK, err)
|
||||
}
|
||||
return candidateRecallResult{
|
||||
Items: r.recallCandidatesFromMySQL(ctx, payload, fact),
|
||||
FallbackMode: "rag_to_mysql",
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 降级:按 user_id + memory_type + status=active 查最近 N 条。
|
||||
return r.recallCandidatesFromMySQL(ctx, payload, fact)
|
||||
return candidateRecallResult{
|
||||
Items: r.recallCandidatesFromMySQL(ctx, payload, fact),
|
||||
FallbackMode: "mysql_only",
|
||||
}
|
||||
}
|
||||
|
||||
// buildCandidatesFromRAG 从 RAG 检索结果构建候选快照列表。
|
||||
|
||||
@@ -6,15 +6,16 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
infrarag "github.com/LoveLosita/smartflow/backend/infra/rag"
|
||||
memorymodel "github.com/LoveLosita/smartflow/backend/memory/model"
|
||||
memoryobserve "github.com/LoveLosita/smartflow/backend/memory/observe"
|
||||
memoryorchestrator "github.com/LoveLosita/smartflow/backend/memory/orchestrator"
|
||||
memoryrepo "github.com/LoveLosita/smartflow/backend/memory/repo"
|
||||
memoryutils "github.com/LoveLosita/smartflow/backend/memory/utils"
|
||||
memoryvectorsync "github.com/LoveLosita/smartflow/backend/memory/vectorsync"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -42,6 +43,9 @@ type Runner struct {
|
||||
extractor Extractor
|
||||
ragRuntime infrarag.Runtime
|
||||
logger *log.Logger
|
||||
vectorSyncer *memoryvectorsync.Syncer
|
||||
observer memoryobserve.Observer
|
||||
metrics memoryobserve.MetricsRecorder
|
||||
|
||||
// 决策层依赖。
|
||||
// 说明:
|
||||
@@ -62,7 +66,16 @@ func NewRunner(
|
||||
ragRuntime infrarag.Runtime,
|
||||
cfg memorymodel.Config,
|
||||
decisionOrchestrator *memoryorchestrator.LLMDecisionOrchestrator,
|
||||
vectorSyncer *memoryvectorsync.Syncer,
|
||||
observer memoryobserve.Observer,
|
||||
metrics memoryobserve.MetricsRecorder,
|
||||
) *Runner {
|
||||
if observer == nil {
|
||||
observer = memoryobserve.NewNopObserver()
|
||||
}
|
||||
if metrics == nil {
|
||||
metrics = memoryobserve.NewNopMetrics()
|
||||
}
|
||||
return &Runner{
|
||||
db: db,
|
||||
jobRepo: jobRepo,
|
||||
@@ -72,6 +85,9 @@ func NewRunner(
|
||||
extractor: extractor,
|
||||
ragRuntime: ragRuntime,
|
||||
logger: log.Default(),
|
||||
vectorSyncer: vectorSyncer,
|
||||
observer: observer,
|
||||
metrics: metrics,
|
||||
cfg: cfg,
|
||||
decisionOrchestrator: decisionOrchestrator,
|
||||
}
|
||||
@@ -96,6 +112,11 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
if job == nil {
|
||||
return &RunOnceResult{Claimed: false}, nil
|
||||
}
|
||||
if job.RetryCount > 0 {
|
||||
r.metrics.AddCounter(memoryobserve.MetricJobRetryTotal, 1, map[string]string{
|
||||
"job_type": strings.TrimSpace(job.JobType),
|
||||
})
|
||||
}
|
||||
|
||||
result := &RunOnceResult{
|
||||
Claimed: true,
|
||||
@@ -110,21 +131,25 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
failReason := fmt.Sprintf("解析任务载荷失败: %v", err)
|
||||
_ = r.jobRepo.MarkFailed(ctx, job.ID, failReason)
|
||||
result.Status = model.MemoryJobStatusFailed
|
||||
r.recordJobOutcome(ctx, job, nil, result.Status, false, err)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 3. 先读取用户记忆设置。总开关关闭时,任务直接成功结束,不再继续抽取和落库。
|
||||
setting, err := r.settingsRepo.GetByUserID(ctx, payload.UserID)
|
||||
if err != nil {
|
||||
r.recordJobOutcome(ctx, job, &payload, model.MemoryJobStatusFailed, false, err)
|
||||
return nil, err
|
||||
}
|
||||
effectiveSetting := memoryutils.EffectiveUserSetting(setting, payload.UserID)
|
||||
if !effectiveSetting.MemoryEnabled {
|
||||
if err = r.jobRepo.MarkSuccess(ctx, job.ID); err != nil {
|
||||
r.recordJobOutcome(ctx, job, &payload, model.MemoryJobStatusFailed, false, err)
|
||||
return nil, err
|
||||
}
|
||||
result.Status = model.MemoryJobStatusSuccess
|
||||
r.logger.Printf("memory worker skipped by user setting: job_id=%d user_id=%d", job.ID, payload.UserID)
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -134,26 +159,31 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
failReason := fmt.Sprintf("抽取执行失败: %v", extractErr)
|
||||
_ = r.jobRepo.MarkFailed(ctx, job.ID, failReason)
|
||||
result.Status = model.MemoryJobStatusFailed
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, false, extractErr)
|
||||
return result, nil
|
||||
}
|
||||
facts = memoryutils.FilterFactsBySetting(facts, effectiveSetting)
|
||||
|
||||
if len(facts) == 0 {
|
||||
if err = r.jobRepo.MarkSuccess(ctx, job.ID); err != nil {
|
||||
r.recordJobOutcome(ctx, job, &payload, model.MemoryJobStatusFailed, false, err)
|
||||
return nil, err
|
||||
}
|
||||
result.Status = model.MemoryJobStatusSuccess
|
||||
r.logger.Printf("memory worker run once noop: job_id=%d", job.ID)
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
items := buildMemoryItems(job, payload, facts)
|
||||
if len(items) == 0 {
|
||||
if err = r.jobRepo.MarkSuccess(ctx, job.ID); err != nil {
|
||||
r.recordJobOutcome(ctx, job, &payload, model.MemoryJobStatusFailed, false, err)
|
||||
return nil, err
|
||||
}
|
||||
result.Status = model.MemoryJobStatusSuccess
|
||||
r.logger.Printf("memory worker run once empty-after-normalize: job_id=%d", job.ID)
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -169,16 +199,19 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
failReason := fmt.Sprintf("决策降级后记忆落库失败: %v", err)
|
||||
_ = r.jobRepo.MarkFailed(ctx, job.ID, failReason)
|
||||
result.Status = model.MemoryJobStatusFailed
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, false, err)
|
||||
return result, nil
|
||||
}
|
||||
result.Status = model.MemoryJobStatusSuccess
|
||||
result.Facts = len(items)
|
||||
r.syncMemoryVectors(ctx, items)
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
// FallbackMode=drop:丢弃本轮抽取结果,直接标记 job 成功。
|
||||
_ = r.jobRepo.MarkSuccess(ctx, job.ID)
|
||||
result.Status = model.MemoryJobStatusSuccess
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -189,6 +222,7 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
r.syncVectorDeletes(ctx, outcome.VectorDeletes)
|
||||
r.logger.Printf("[去重] 决策流程完成: job_id=%d user_id=%d 新增=%d 更新=%d 删除=%d 跳过=%d",
|
||||
job.ID, payload.UserID, outcome.AddCount, outcome.UpdateCount, outcome.DeleteCount, outcome.NoneCount)
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -197,6 +231,7 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
failReason := fmt.Sprintf("记忆落库失败: %v", err)
|
||||
_ = r.jobRepo.MarkFailed(ctx, job.ID, failReason)
|
||||
result.Status = model.MemoryJobStatusFailed
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, false, err)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -204,6 +239,7 @@ func (r *Runner) RunOnce(ctx context.Context) (*RunOnceResult, error) {
|
||||
result.Facts = len(items)
|
||||
r.syncMemoryVectors(ctx, items)
|
||||
r.logger.Printf("memory worker run once success: job_id=%d extracted_facts=%d", job.ID, len(items))
|
||||
r.recordJobOutcome(ctx, job, &payload, result.Status, true, nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -268,56 +304,10 @@ func buildMemoryItems(job *model.MemoryJob, payload memorymodel.ExtractJobPayloa
|
||||
}
|
||||
|
||||
func (r *Runner) syncMemoryVectors(ctx context.Context, items []model.MemoryItem) {
|
||||
if r == nil || r.ragRuntime == nil || r.itemRepo == nil || len(items) == 0 {
|
||||
if r == nil || r.vectorSyncer == nil || len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
requestItems := make([]infrarag.MemoryIngestItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
requestItems = append(requestItems, infrarag.MemoryIngestItem{
|
||||
MemoryID: item.ID,
|
||||
UserID: item.UserID,
|
||||
ConversationID: strValue(item.ConversationID),
|
||||
AssistantID: strValue(item.AssistantID),
|
||||
RunID: strValue(item.RunID),
|
||||
MemoryType: item.MemoryType,
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
Confidence: item.Confidence,
|
||||
Importance: item.Importance,
|
||||
SensitivityLevel: item.SensitivityLevel,
|
||||
IsExplicit: item.IsExplicit,
|
||||
Status: item.Status,
|
||||
TTLAt: item.TTLAt,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
result, err := r.ragRuntime.IngestMemory(ctx, infrarag.MemoryIngestRequest{
|
||||
Action: "add",
|
||||
Items: requestItems,
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Printf("[WARN][去重] 记忆向量同步失败: count=%d err=%v", len(items), err)
|
||||
for _, item := range items {
|
||||
_ = r.itemRepo.UpdateVectorStateByID(ctx, item.ID, "failed", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
vectorIDMap := make(map[int64]string, len(result.DocumentIDs))
|
||||
for _, documentID := range result.DocumentIDs {
|
||||
memoryID := parseMemoryID(documentID)
|
||||
if memoryID <= 0 {
|
||||
continue
|
||||
}
|
||||
vectorIDMap[memoryID] = documentID
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
vectorID := strPtrOrNil(vectorIDMap[item.ID])
|
||||
_ = r.itemRepo.UpdateVectorStateByID(ctx, item.ID, "synced", vectorID)
|
||||
}
|
||||
r.vectorSyncer.Upsert(ctx, "", items)
|
||||
}
|
||||
|
||||
// syncVectorDeletes 处理决策层 DELETE 动作产出的向量清理需求。
|
||||
@@ -327,33 +317,10 @@ func (r *Runner) syncMemoryVectors(ctx context.Context, items []model.MemoryItem
|
||||
// 2. 调 Runtime.DeleteMemory 真正从 Milvus 删除对应向量;
|
||||
// 3. 更新 MySQL vector_status 标记删除结果。
|
||||
func (r *Runner) syncVectorDeletes(ctx context.Context, memoryIDs []int64) {
|
||||
if r == nil || len(memoryIDs) == 0 {
|
||||
if r == nil || r.vectorSyncer == nil || len(memoryIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 构造 documentID 列表。
|
||||
documentIDs := make([]string, 0, len(memoryIDs))
|
||||
for _, id := range memoryIDs {
|
||||
documentIDs = append(documentIDs, fmt.Sprintf("memory:%d", id))
|
||||
}
|
||||
|
||||
// 2. 调 Runtime 删除向量。
|
||||
if r.ragRuntime != nil {
|
||||
if err := r.ragRuntime.DeleteMemory(ctx, documentIDs); err != nil {
|
||||
r.logger.Printf("[WARN][去重] Milvus 向量删除失败,标记为 pending 等待后续清理: count=%d ids=%v err=%v", len(memoryIDs), memoryIDs, err)
|
||||
} else {
|
||||
r.logger.Printf("[去重] Milvus 向量删除完成: count=%d ids=%v", len(memoryIDs), memoryIDs)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 更新 MySQL vector_status。
|
||||
for _, memoryID := range memoryIDs {
|
||||
if updateErr := r.itemRepo.UpdateVectorStateByID(ctx, memoryID, "deleted", nil); updateErr != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Printf("[WARN] 向量状态更新失败: memory_id=%d err=%v", memoryID, updateErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
r.vectorSyncer.Delete(ctx, "", memoryIDs)
|
||||
}
|
||||
|
||||
func resolveMemoryTTLAt(base time.Time, memoryType string) *time.Time {
|
||||
@@ -395,11 +362,106 @@ func int64PtrOrNil(v int64) *int64 {
|
||||
return &value
|
||||
}
|
||||
|
||||
func strValue(v *string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
func (r *Runner) recordJobOutcome(
|
||||
ctx context.Context,
|
||||
job *model.MemoryJob,
|
||||
payload *memorymodel.ExtractJobPayload,
|
||||
status string,
|
||||
success bool,
|
||||
err error,
|
||||
) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
return strings.TrimSpace(*v)
|
||||
|
||||
level := memoryobserve.LevelInfo
|
||||
if !success || err != nil {
|
||||
level = memoryobserve.LevelWarn
|
||||
}
|
||||
fields := map[string]any{
|
||||
"job_id": jobIDValue(job),
|
||||
"status": strings.TrimSpace(status),
|
||||
"success": success && err == nil,
|
||||
"error": err,
|
||||
"error_code": memoryobserve.ClassifyError(err),
|
||||
}
|
||||
if payload != nil {
|
||||
fields["trace_id"] = strings.TrimSpace(payload.TraceID)
|
||||
fields["user_id"] = payload.UserID
|
||||
fields["conversation_id"] = strings.TrimSpace(payload.ConversationID)
|
||||
}
|
||||
|
||||
r.observer.Observe(ctx, memoryobserve.Event{
|
||||
Level: level,
|
||||
Component: memoryobserve.ComponentWrite,
|
||||
Operation: "job",
|
||||
Fields: fields,
|
||||
})
|
||||
r.metrics.AddCounter(memoryobserve.MetricJobTotal, 1, map[string]string{
|
||||
"status": strings.TrimSpace(status),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Runner) recordDecisionObservation(
|
||||
ctx context.Context,
|
||||
job *model.MemoryJob,
|
||||
payload memorymodel.ExtractJobPayload,
|
||||
fact memorymodel.NormalizedFact,
|
||||
candidateCount int,
|
||||
finalAction string,
|
||||
fallbackMode string,
|
||||
success bool,
|
||||
err error,
|
||||
) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
level := memoryobserve.LevelInfo
|
||||
status := "success"
|
||||
if !success || err != nil {
|
||||
level = memoryobserve.LevelWarn
|
||||
status = "error"
|
||||
}
|
||||
fallbackMode = strings.TrimSpace(fallbackMode)
|
||||
if fallbackMode == "" {
|
||||
fallbackMode = "none"
|
||||
}
|
||||
|
||||
r.observer.Observe(ctx, memoryobserve.Event{
|
||||
Level: level,
|
||||
Component: memoryobserve.ComponentWrite,
|
||||
Operation: memoryobserve.OperationDecision,
|
||||
Fields: map[string]any{
|
||||
"trace_id": strings.TrimSpace(payload.TraceID),
|
||||
"user_id": payload.UserID,
|
||||
"conversation_id": strings.TrimSpace(payload.ConversationID),
|
||||
"job_id": jobIDValue(job),
|
||||
"fact_type": strings.TrimSpace(fact.MemoryType),
|
||||
"candidate_count": candidateCount,
|
||||
"final_action": strings.TrimSpace(finalAction),
|
||||
"fallback_mode": fallbackMode,
|
||||
"success": success && err == nil,
|
||||
"error": err,
|
||||
"error_code": memoryobserve.ClassifyError(err),
|
||||
},
|
||||
})
|
||||
r.metrics.AddCounter(memoryobserve.MetricDecisionTotal, 1, map[string]string{
|
||||
"action": strings.TrimSpace(finalAction),
|
||||
"status": status,
|
||||
})
|
||||
if fallbackMode != "none" && fallbackMode != "hash_exact" && fallbackMode != "rag" {
|
||||
r.metrics.AddCounter(memoryobserve.MetricDecisionFallbackTotal, 1, map[string]string{
|
||||
"mode": fallbackMode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func jobIDValue(job *model.MemoryJob) int64 {
|
||||
if job == nil {
|
||||
return 0
|
||||
}
|
||||
return job.ID
|
||||
}
|
||||
|
||||
func parseMemoryID(documentID string) int64 {
|
||||
@@ -411,9 +473,13 @@ func parseMemoryID(documentID string) int64 {
|
||||
if strings.HasPrefix(raw, "uid:") {
|
||||
return 0
|
||||
}
|
||||
memoryID, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
|
||||
var value int64
|
||||
for _, ch := range raw {
|
||||
if ch < '0' || ch > '9' {
|
||||
return 0
|
||||
}
|
||||
value = value*10 + int64(ch-'0')
|
||||
}
|
||||
return memoryID
|
||||
return value
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user