diff --git a/go/internal/api/routes/skill_crystallize.go b/go/internal/api/routes/skill_crystallize.go index 1edc77b..864980d 100644 --- a/go/internal/api/routes/skill_crystallize.go +++ b/go/internal/api/routes/skill_crystallize.go @@ -73,6 +73,10 @@ func isMemoryLinked(memID string) bool { // CrystallizeSkill 对指定记忆执行结晶(HTTP 路由) func CrystallizeSkill(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + respondError(w, 405, "POST required") + return + } memID := r.PathValue("id") if memID == "" { respondError(w, 400, "memory id required") diff --git a/go/internal/api/server.go b/go/internal/api/server.go index a4cb5a6..15cddc1 100644 --- a/go/internal/api/server.go +++ b/go/internal/api/server.go @@ -693,7 +693,7 @@ func NewServer() http.Handler { mux.HandleFunc("/api/v1/skills/{name}/execute", routes.ExecuteSkill) // G7.3 skill执行 // G7.2: 结晶路由 mux.HandleFunc("/api/v1/crystallize/candidates", routes.GetSkillCandidates) // 获取候选 - mux.HandleFunc("POST /api/v1/crystallize/memory/{id}", routes.CrystallizeSkill) // 对记忆执行结晶 + mux.HandleFunc("/api/v1/crystallize/memory/{id}", routes.CrystallizeSkill) // 对记忆执行结晶 // 评估 + 自调参 mux.HandleFunc("/api/v1/eval/run", evalAPI.Run) diff --git a/go/internal/models/memory.go b/go/internal/models/memory.go index eba22c4..d31f4a9 100644 --- a/go/internal/models/memory.go +++ b/go/internal/models/memory.go @@ -3,6 +3,27 @@ package models import "time" +// Time 是 time.Time 的安全版本,支持空字符串解析 +type Time struct{ time.Time } + +// UnmarshalJSON 实现 json.Unmarshaler:空字符串解析为当前时间(而非报错) +func (t *Time) UnmarshalJSON(data []byte) error { + s := string(data) + if s == `""` || s == `"0001-01-01T00:00:00Z"` { + t.Time = time.Time{} + return nil + } + // 去掉前后引号 + if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { + s = s[1 : len(s)-1] + } + if s == "" || s == "null" { + t.Time = time.Time{} + return nil + } + return t.Time.UnmarshalJSON(data) +} + // VersionRecord 记忆版本记录(溯源链中的单次修改) type VersionRecord struct { Version int `json:"version"` diff --git a/go/internal/storage/lancedb_ipc.go b/go/internal/storage/lancedb_ipc.go index 5c440ab..151b1b0 100644 --- a/go/internal/storage/lancedb_ipc.go +++ b/go/internal/storage/lancedb_ipc.go @@ -42,6 +42,8 @@ type ipcReq struct { Namespace string `json:"namespace,omitempty"` ID string `json:"id,omitempty"` Fields string `json:"fields,omitempty"` + MinRecall int `json:"min_recall,omitempty"` + QueryLimit int `json:"limit,omitempty"` } type ipcResp struct { @@ -465,20 +467,112 @@ func (rc *RustLanceDBClient) GetCandidatesForForgetting() ([]map[string]interfac return out, nil } -func (rc *RustLanceDBClient) GetSkillCandidates(minRecalls, limit int) ([]models.MemoryRecord, error) { - _local.mu.RLock() - defer _local.mu.RUnlock() - var out []models.MemoryRecord - for _, m := range _local.memories { - if m.IsDeleted || m.Tier == "core" || m.RecallCount < minRecalls { - continue - } - out = append(out, *m) - if len(out) >= limit { - break +func mapStr(m map[string]interface{}, key string) string { + if v, ok := m[key]; ok { + if s, ok := v.(string); ok { + return s } } - return out, nil + return "" +} +func mapFloat(m map[string]interface{}, key string) float64 { + if v, ok := m[key]; ok { + if f, ok := v.(float64); ok { + return f + } + } + return 0 +} +func mapInt(m map[string]interface{}, key string) int { + if v, ok := m[key]; ok { + if f, ok := v.(float64); ok { + return int(f) + } + } + return 0 +} +func mapBool(m map[string]interface{}, key string) bool { + if v, ok := m[key]; ok { + if b, ok := v.(bool); ok { + return b + } + } + return false +} + +func (rc *RustLanceDBClient) GetSkillCandidates(minRecalls, limit int) ([]models.MemoryRecord, error) { + resp, err := rc.rpc(ipcReq{ + Type: "lancedb_query", + MinRecall: minRecalls, + QueryLimit: limit, + }) + if err != nil { + return nil, fmt.Errorf("lancedb_query: %w", err) + } + raw := resp.ReportJSON + if raw == "" { + return []models.MemoryRecord{}, nil + } + + // 用 raw JSON 反序列化,避免 time.Time 空字符串报错 + var rawRecords []map[string]interface{} + if err := json.Unmarshal([]byte(raw), &rawRecords); err != nil { + return nil, fmt.Errorf("parse lancedb_query result: %w", err) + } + if len(rawRecords) == 0 { + return []models.MemoryRecord{}, nil + } + + records := make([]models.MemoryRecord, 0, len(rawRecords)) + for _, r := range rawRecords { + m := models.MemoryRecord{ + ID: mapStr(r, "id"), + AgentID: mapStr(r, "agent_id"), + Namespace: mapStr(r, "namespace"), + Content: mapStr(r, "content"), + Category: mapStr(r, "category"), + Tier: mapStr(r, "tier"), + Source: mapStr(r, "source"), + Freshness: mapStr(r, "freshness"), + DerivedFrom: mapStr(r, "derived_from"), + VersionHistory: nil, + DependsOn: nil, + } + m.QualityScore = mapFloat(r, "quality_score") + m.Importance = mapFloat(r, "importance") + m.UsefulCount = mapInt(r, "useful_count") + m.NotUsefulCount = mapInt(r, "not_useful_count") + m.RecallCount = mapInt(r, "recall_count") + m.Version = mapInt(r, "version") + m.VolatileFlag = mapBool(r, "volatile_flag") + m.IsDeleted = mapBool(r, "is_deleted") + // 时间字段:空字符串 → 零值 time.Time + if t := mapStr(r, "last_recalled_at"); t != "" { + if pt, err := time.Parse(time.RFC3339, t); err == nil { + m.LastRecalledAt = pt + } + } + if t := mapStr(r, "created_at"); t != "" { + if pt, err := time.Parse(time.RFC3339, t); err == nil { + m.CreatedAt = pt + } + } + if t := mapStr(r, "updated_at"); t != "" { + if pt, err := time.Parse(time.RFC3339, t); err == nil { + m.UpdatedAt = pt + } + } + records = append(records, m) + } + + // 同步到本地缓存 + _local.mu.Lock() + for _, m := range records { + cp := m + _local.memories[m.ID] = &cp + } + _local.mu.Unlock() + return records, nil } func (rc *RustLanceDBClient) Backup(path string) error { return nil } diff --git a/rust/src/lancedb_ops.rs b/rust/src/lancedb_ops.rs index 3ee4e12..f71b3f6 100644 --- a/rust/src/lancedb_ops.rs +++ b/rust/src/lancedb_ops.rs @@ -131,6 +131,52 @@ impl LanceDBOps { Ok(records) } + /// 查询记忆(支持 min_recall_count 过滤),供 GetSkillCandidates 使用 + pub fn query_memories(&self, min_recall: i64, limit: usize) -> Result, Box> { + let db = rt().block_on(lancedb::connect(self.data_dir.to_str().unwrap()).execute())?; + let tbl = rt().block_on(db.open_table("memories").execute())?; + + let filter = format!("recall_count >= {} AND is_deleted = false AND tier != 'core'", min_recall); + let mut results = Box::pin(rt().block_on( + tbl.query() + .only_if(&filter) + .limit(limit) + .execute() + )?); + + let mut records = Vec::new(); + while let Some(Ok(batch)) = rt().block_on(results.next()) { + for i in 0..batch.num_rows() { + records.push(MemoryRecord { + id: col_str(&batch, i, "id"), + agent_id: col_str(&batch, i, "agent_id"), + namespace: col_str(&batch, i, "namespace"), + content: col_str(&batch, i, "content"), + category: col_str(&batch, i, "category"), + vector: col_vector(&batch, i, "vector"), + tier: col_str(&batch, i, "tier"), + importance: col_f64(&batch, i, "importance"), + quality_score: col_f64(&batch, i, "quality_score"), + recall_count: col_i64(&batch, i, "recall_count"), + useful_count: col_i64(&batch, i, "useful_count"), + not_useful_count: col_i64(&batch, i, "not_useful_count"), + freshness: col_str(&batch, i, "freshness"), + version: col_i64(&batch, i, "version"), + version_history: String::new(), + source: col_str(&batch, i, "source"), + volatile_flag: col_bool(&batch, i, "volatile_flag"), + is_deleted: col_bool(&batch, i, "is_deleted"), + depends_on: String::new(), + derived_from: String::new(), + last_recalled_at: String::new(), + created_at: col_str(&batch, i, "created_at"), + updated_at: col_str(&batch, i, "updated_at"), + }); + } + } + Ok(records) + } + pub fn insert_batch(&self, table: &str, records_json: &str) -> Result> { let values: serde_json::Value = serde_json::from_str(records_json)?; let records: Vec = values.as_array().ok_or("expected JSON array")?.to_vec(); diff --git a/rust/src/main.rs b/rust/src/main.rs index f3038d9..b5ebd2c 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -300,6 +300,18 @@ fn handle_client(mut stream: UnixStream, args: &Args, lancedb: LanceDBOps, state } } } + "lancedb_query" => { + let min_recall = msg["min_recall"].as_i64().unwrap_or(5) as i64; + let limit = msg["limit"].as_u64().unwrap_or(20) as usize; + eprintln!("[lancedb] query: min_recall={} limit={}", min_recall, limit); + match lancedb.query_memories(min_recall, limit) { + Ok(r) => { + eprintln!("[lancedb] query returned {} records", r.len()); + send_ok(&mut stream, &serde_json::to_string(&r).unwrap_or_default()) + } + Err(e) => send_error(&mut stream, "lancedb_query", &e.to_string()), + } + } _ => { let req: ConsolidateRequest = match serde_json::from_value(msg) { Ok(r) => r, Err(e) => {