mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
f79782a008
* Resolves issue #13 - Switched model to bge-small-en-v1.5 - Added lazy re-embedding - Added model version tracking per vector - Added conversion of vectors to the new model * Add lfs support to the workflow. * Implements importance scoring with decay + voting #6 * Resolves issue #5 by marking observations as superseeded and scheduled for deletion * Implement pattern detection #7 * Improve injections and observations accuracy - Session start: Recent observations for project context (recency-based) - User prompt: Semantically relevant observations (similarity-based with threshold) * Added two stage retrieval with bi and cross encoder #8 * Implement query expansion and reformulation #9 * Knowledge graph and relationships ( resolves #4 ) - File Overlap Detection: Detects relationships when observations modify/read the same files - Concept Overlap Detection: Detects relationships based on shared semantic concepts - Type Progression Detection: Infers relationships from natural observation type progressions (e.g., discovery → bugfix = "fixes") - Temporal Proximity Detection: Detects relationships between observations in the same session within 5 minutes - Narrative Mention Detection: Detects explicit relationship language in narratives (e.g., "fixes", "depends on", "supersedes") * Add visualisation of the relations to the dashboard. * fixup! Add visualisation of the relations to the dashboard. * Update documentation with new settings and screenshots.
293 lines
7.3 KiB
Go
293 lines
7.3 KiB
Go
// Package worker provides the main worker service for claude-mnemonic.
|
|
package worker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
|
)
|
|
|
|
// DefaultPatternsLimit is the default number of patterns to return.
|
|
const DefaultPatternsLimit = 100
|
|
|
|
// handleGetPatterns returns all active patterns, optionally filtered by type or project.
|
|
func (s *Service) handleGetPatterns(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
// Parse query parameters
|
|
limit := DefaultPatternsLimit
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
patternType := r.URL.Query().Get("type")
|
|
project := r.URL.Query().Get("project")
|
|
|
|
var patterns []*models.Pattern
|
|
var err error
|
|
|
|
if patternType != "" {
|
|
// Filter by type
|
|
patterns, err = store.GetPatternsByType(r.Context(), models.PatternType(patternType), limit)
|
|
} else if project != "" {
|
|
// Filter by project
|
|
patterns, err = store.GetPatternsByProject(r.Context(), project, limit)
|
|
} else {
|
|
// Get all active patterns
|
|
patterns, err = store.GetActivePatterns(r.Context(), limit)
|
|
}
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, patterns)
|
|
}
|
|
|
|
// handleGetPatternStats returns aggregate statistics about patterns.
|
|
func (s *Service) handleGetPatternStats(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
stats, err := store.GetPatternStats(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, stats)
|
|
}
|
|
|
|
// handleGetPatternByID returns a single pattern by ID.
|
|
func (s *Service) handleGetPatternByID(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid pattern ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
pattern, err := store.GetPatternByID(r.Context(), id)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if pattern == nil {
|
|
http.Error(w, "pattern not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, pattern)
|
|
}
|
|
|
|
// handleGetPatternInsight returns a formatted insight string for a pattern.
|
|
func (s *Service) handleGetPatternInsight(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
detector := s.patternDetector
|
|
s.initMu.RUnlock()
|
|
|
|
if detector == nil {
|
|
http.Error(w, "pattern detector not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid pattern ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
insight, err := detector.GetPatternInsight(r.Context(), id)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, map[string]string{"insight": insight})
|
|
}
|
|
|
|
// handleDeletePattern deletes a pattern by ID.
|
|
func (s *Service) handleDeletePattern(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid pattern ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := store.DeletePattern(r.Context(), id); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, map[string]string{"status": "deleted"})
|
|
}
|
|
|
|
// handleDeprecatePattern marks a pattern as deprecated.
|
|
func (s *Service) handleDeprecatePattern(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "invalid pattern ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := store.MarkPatternDeprecated(r.Context(), id); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, map[string]string{"status": "deprecated"})
|
|
}
|
|
|
|
// MergePatternsRequest is the request body for merging patterns.
|
|
type MergePatternsRequest struct {
|
|
SourceID int64 `json:"source_id"`
|
|
TargetID int64 `json:"target_id"`
|
|
}
|
|
|
|
// handleSearchPatterns performs full-text search on patterns.
|
|
func (s *Service) handleSearchPatterns(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
query := r.URL.Query().Get("q")
|
|
if query == "" {
|
|
http.Error(w, "query parameter 'q' is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
limit := DefaultPatternsLimit
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
patterns, err := store.SearchPatternsFTS(r.Context(), query, limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, patterns)
|
|
}
|
|
|
|
// handleGetPatternByName returns a pattern by its name.
|
|
func (s *Service) handleGetPatternByName(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
name := r.URL.Query().Get("name")
|
|
if name == "" {
|
|
http.Error(w, "query parameter 'name' is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
pattern, err := store.GetPatternByName(r.Context(), name)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if pattern == nil {
|
|
http.Error(w, "pattern not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, pattern)
|
|
}
|
|
|
|
// handleMergePatterns merges a source pattern into a target pattern.
|
|
func (s *Service) handleMergePatterns(w http.ResponseWriter, r *http.Request) {
|
|
s.initMu.RLock()
|
|
store := s.patternStore
|
|
s.initMu.RUnlock()
|
|
|
|
if store == nil {
|
|
http.Error(w, "pattern store not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
var req MergePatternsRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.SourceID == 0 || req.TargetID == 0 {
|
|
http.Error(w, "source_id and target_id are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.SourceID == req.TargetID {
|
|
http.Error(w, "source_id and target_id cannot be the same", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := store.MergePatterns(r.Context(), req.SourceID, req.TargetID); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, map[string]string{"status": "merged"})
|
|
}
|