(ctx context.Context, query, category string)
| 180 | } |
| 181 | |
| 182 | func (m *MemoryDatabase) SearchMemories(ctx context.Context, query, category string) (results []database.UserMemory, err error) { |
| 183 | // SearchMemories is the retrieval shape per the OTel GenAI semconv: |
| 184 | // the agent is recalling stored memories filtered by query/category. |
| 185 | // Use the spec'd `retrieval {data_source.id}` span so this lands on |
| 186 | // the same dashboard row as RAG retrievals. |
| 187 | ctx, retSpan := genai.StartRetrieval(ctx, "sqlite", memoryDataSourceID, false, "") |
| 188 | defer func() { |
| 189 | if err != nil { |
| 190 | retSpan.RecordError(err, "") |
| 191 | } |
| 192 | retSpan.SetResultCount(len(results)) |
| 193 | retSpan.End() |
| 194 | }() |
| 195 | if category != "" { |
| 196 | retSpan.SetAttributes(attribute.String("cagent.memory.category", category)) |
| 197 | } |
| 198 | |
| 199 | // Assign to the named returns (not local shadows) so the deferred |
| 200 | // span closure observes the live error and result count regardless |
| 201 | // of which return path fires. |
| 202 | var conditions []string |
| 203 | var args []any |
| 204 | |
| 205 | if query != "" { |
| 206 | words := strings.FieldsSeq(query) |
| 207 | for word := range words { |
| 208 | conditions = append(conditions, "LOWER(memory) LIKE LOWER(?) ESCAPE '\\'") |
| 209 | escaped := strings.ReplaceAll(word, `\`, `\\`) |
| 210 | escaped = strings.ReplaceAll(escaped, `%`, `\%`) |
| 211 | escaped = strings.ReplaceAll(escaped, `_`, `\_`) |
| 212 | args = append(args, "%"+escaped+"%") |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | if category != "" { |
| 217 | conditions = append(conditions, "LOWER(category) = LOWER(?)") |
| 218 | args = append(args, category) |
| 219 | } |
| 220 | |
| 221 | stmt := "SELECT id, created_at, memory, COALESCE(category, '') FROM memories" |
| 222 | if len(conditions) > 0 { |
| 223 | stmt += " WHERE " + strings.Join(conditions, " AND ") //nolint:gosec // conditions are internal SQL fragments; values are bound parameters |
| 224 | } |
| 225 | |
| 226 | var rows *sql.Rows |
| 227 | db, err := m.ensureDB(ctx) |
| 228 | if err != nil { |
| 229 | return nil, err |
| 230 | } |
| 231 | rows, err = db.QueryContext(ctx, stmt, args...) |
| 232 | if err != nil { |
| 233 | return nil, err |
| 234 | } |
| 235 | defer rows.Close() |
| 236 | |
| 237 | for rows.Next() { |
| 238 | var memory database.UserMemory |
| 239 | // gocritic suggests `:=` here, but we want to assign to the |
nothing calls this directly
no test coverage detected