-
Notifications
You must be signed in to change notification settings - Fork 11
Create cache_embedding.go #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
drQedwards
wants to merge
1
commit into
supermodeltools:main
Choose a base branch
from
drQedwards:patch-2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+309
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,309 @@ | ||
| // embeddingcache/embedding_cache.go | ||
| // Semantic Tool Output Caching with embedding similarity (inspired by Context+ patterns) | ||
|
|
||
| package embeddingcache | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "math" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "supermodeltools/cli/embeddings" // Adjust to your actual embeddings package (Context+-style) | ||
| ) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Configuration | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const ( | ||
| SimilarityThreshold = 0.95 | ||
| DefaultTTL = 300 * time.Second // 5 minutes | ||
| MaxEntriesPerTool = 50 | ||
| ) | ||
|
|
||
| var ToolTTL = map[string]time.Duration{ | ||
| "web_search": 10 * time.Minute, | ||
| "web_fetch": 15 * time.Minute, | ||
| "kb_search": 2 * time.Minute, | ||
| "file_read": 1 * time.Minute, | ||
| "file_list": 1 * time.Minute, | ||
| "ref_lookup": 10 * time.Minute, | ||
| "chart_snapshot": 5 * time.Minute, | ||
| } | ||
|
|
||
| var UncacheableTools = map[string]struct{}{ | ||
| "file_write": {}, | ||
| "file_patch": {}, | ||
| "file_append": {}, | ||
| "file_delete": {}, | ||
| "file_rename": {}, | ||
| "file_copy": {}, | ||
| "code_run": {}, | ||
| "kb_save": {}, | ||
| "kb_update": {}, | ||
| "kb_delete": {}, | ||
| "agent_msg": {}, | ||
| "inbox_send": {}, | ||
| "agent_create": {}, | ||
| "agent_deactivate": {}, | ||
| "schedule_task": {}, | ||
| "schedule_cancel": {}, | ||
| "plan_create": {}, | ||
| "plan_update": {}, | ||
| "comfyui_generate": {}, | ||
| "git_commit": {}, | ||
| "issue_create": {}, | ||
| "issue_update": {}, | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Types | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| type CacheEntry struct { | ||
| ParamsVec []float32 `json:"params_vec"` | ||
| ParamsText string `json:"params_text"` | ||
| Result string `json:"result"` | ||
| Timestamp time.Time `json:"timestamp"` | ||
| } | ||
|
|
||
| type Stats struct { | ||
| Hits int64 `json:"hits"` | ||
| Misses int64 `json:"misses"` | ||
| SkippedUncacheable int64 `json:"skipped_uncacheable"` | ||
| mu sync.Mutex | ||
| } | ||
|
|
||
| type Cache struct { | ||
| mu sync.RWMutex | ||
| data map[string][]CacheEntry | ||
| stats Stats | ||
| } | ||
|
|
||
| // New creates a new semantic embedding cache. | ||
| func New() *Cache { | ||
| return &Cache{ | ||
| data: make(map[string][]CacheEntry), | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Core Methods | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // CheckCache returns cached result if a semantically similar call exists, else nil. | ||
| func (c *Cache) CheckCache(toolName, paramsText string) *string { | ||
| if _, ok := UncacheableTools[toolName]; ok { | ||
| c.stats.incSkipped() | ||
| return nil | ||
| } | ||
|
|
||
| c.mu.RLock() | ||
| entries, exists := c.data[toolName] | ||
| c.mu.RUnlock() | ||
|
|
||
| if !exists || len(entries) == 0 { | ||
| c.stats.incMiss() | ||
| return nil | ||
| } | ||
|
|
||
| if !embeddings.IsEnabled() { | ||
| c.stats.incMiss() | ||
| return nil | ||
| } | ||
|
|
||
| // Create cache key (same as Python) | ||
| cacheKey := fmt.Sprintf("%s: %s", toolName, truncate(paramsText, 500)) | ||
| vec, err := embeddings.EmbedText(cacheKey) | ||
| if err != nil || vec == nil { | ||
| c.stats.incMiss() | ||
| return nil | ||
| } | ||
|
|
||
| ttl := getTTL(toolName) | ||
| now := time.Now() | ||
|
|
||
| var bestSim float32 | ||
| var bestResult *string | ||
|
|
||
| c.mu.RLock() | ||
| for i := range entries { | ||
| e := &entries[i] | ||
| if now.Sub(e.Timestamp) > ttl { | ||
| continue | ||
| } | ||
| sim := embeddings.CosineSimilarity(vec, e.ParamsVec) | ||
| if sim > bestSim { | ||
| bestSim = sim | ||
| if sim >= SimilarityThreshold { | ||
| bestResult = &e.Result | ||
| } | ||
| } | ||
| } | ||
| c.mu.RUnlock() | ||
|
|
||
| if bestResult != nil { | ||
| c.stats.incHit() | ||
| age := int(time.Since(entries[0].Timestamp).Seconds()) // approximate | ||
| fmt.Printf(" [EmbCache] HIT for %s (sim=%.3f, age=%ds)\n", toolName, bestSim, age) | ||
| return bestResult | ||
| } | ||
|
|
||
| c.stats.incMiss() | ||
| return nil | ||
| } | ||
|
|
||
| // StoreResult stores the result if it's cacheable. | ||
| func (c *Cache) StoreResult(toolName, paramsText, result string) { | ||
| if _, ok := UncacheableTools[toolName]; ok { | ||
| return | ||
| } | ||
| if isErrorResult(result) { | ||
| return | ||
| } | ||
| if !embeddings.IsEnabled() { | ||
| return | ||
| } | ||
|
|
||
| cacheKey := fmt.Sprintf("%s: %s", toolName, truncate(paramsText, 500)) | ||
| vec, err := embeddings.EmbedText(cacheKey) | ||
| if err != nil || vec == nil { | ||
| return | ||
| } | ||
|
|
||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
|
|
||
| if _, exists := c.data[toolName]; !exists { | ||
| c.data[toolName] = make([]CacheEntry, 0, MaxEntriesPerTool) | ||
| } | ||
|
|
||
| entries := c.data[toolName] | ||
| entries = append(entries, CacheEntry{ | ||
| ParamsVec: vec, | ||
| ParamsText: truncate(paramsText, 200), | ||
| Result: result, | ||
| Timestamp: time.Now(), | ||
| }) | ||
|
|
||
| if len(entries) > MaxEntriesPerTool { | ||
| entries = entries[len(entries)-MaxEntriesPerTool:] | ||
| } | ||
|
|
||
| c.data[toolName] = entries | ||
| } | ||
|
|
||
| // Clear clears cache for a specific tool or entirely. | ||
| func (c *Cache) Clear(toolName string) { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| if toolName != "" { | ||
| delete(c.data, toolName) | ||
| } else { | ||
| c.data = make(map[string][]CacheEntry) | ||
| } | ||
| } | ||
|
|
||
| // EvictExpired removes stale entries (call periodically). | ||
| func (c *Cache) EvictExpired() { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
|
|
||
| now := time.Now() | ||
| for tool, entries := range c.data { | ||
| ttl := getTTL(tool) | ||
| filtered := entries[:0] | ||
| for _, e := range entries { | ||
| if now.Sub(e.Timestamp) <= ttl { | ||
| filtered = append(filtered, e) | ||
| } | ||
| } | ||
| if len(filtered) == 0 { | ||
| delete(c.data, tool) | ||
| } else { | ||
| c.data[tool] = filtered | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // GetStats returns current statistics. | ||
| func (c *Cache) GetStats() map[string]any { | ||
| c.stats.mu.Lock() | ||
| defer c.stats.mu.Unlock() | ||
|
|
||
| total := c.stats.Hits + c.stats.Misses | ||
| hitRate := 0.0 | ||
| if total > 0 { | ||
| hitRate = float64(c.stats.Hits) / float64(total) * 100 | ||
| } | ||
|
|
||
| c.mu.RLock() | ||
| cachedTools := make([]string, 0, len(c.data)) | ||
| totalEntries := 0 | ||
| for t, e := range c.data { | ||
| cachedTools = append(cachedTools, t) | ||
| totalEntries += len(e) | ||
| } | ||
| c.mu.RUnlock() | ||
|
|
||
| return map[string]any{ | ||
| "hits": c.stats.Hits, | ||
| "misses": c.stats.Misses, | ||
| "skipped_uncacheable": c.stats.SkippedUncacheable, | ||
| "hit_rate_pct": math.Round(hitRate*10) / 10, | ||
| "cached_tools": cachedTools, | ||
| "total_entries": totalEntries, | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Internal helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| func getTTL(toolName string) time.Duration { | ||
| if d, ok := ToolTTL[toolName]; ok { | ||
| return d | ||
| } | ||
| return DefaultTTL | ||
| } | ||
|
|
||
| func truncate(s string, n int) string { | ||
| if len(s) > n { | ||
| return s[:n] | ||
| } | ||
| return s | ||
| } | ||
|
|
||
| func isErrorResult(s string) bool { | ||
| if s == "" { | ||
| return false | ||
| } | ||
| lower := strings.ToLower(s) | ||
| for _, word := range []string{"error", "traceback", "failed", "exception"} { | ||
| if strings.Contains(lower, word) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // Stats helpers | ||
| func (s *Stats) incHit() { | ||
| s.mu.Lock() | ||
| s.Hits++ | ||
| s.mu.Unlock() | ||
| } | ||
|
|
||
| func (s *Stats) incMiss() { | ||
| s.mu.Lock() | ||
| s.Misses++ | ||
| s.mu.Unlock() | ||
| } | ||
|
|
||
| func (s *Stats) incSkipped() { | ||
| s.mu.Lock() | ||
| s.SkippedUncacheable++ | ||
| s.mu.Unlock() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this file under
cmd/withpackage embeddingcachemakes the directory contain bothcmdandembeddingcachepackages; the rest of this directory's non-test files arepackage cmd, so building or testing the CLI will fail before this cache can be used. Move the cache into its own directory/package or change the package name to match the directory's existing package.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay @greynewell I dunno what would be the best file to change this into. your suggestion?