diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-19 10:51:13 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-19 10:51:13 -0500 |
| commit | a960a0de7abc50abec51262de8a5871048817f1f (patch) | |
| tree | 973f4fbc8d75031bbb01ed1aec62a23c85fa2919 /Omni/Agent/Tools/AvaLogs.hs | |
| parent | 533e4209192298de4808c58f6ea6244e4bed5768 (diff) | |
Add semantic search for chat history
- Add chat_history table with embeddings in memory.db
- Add saveChatHistoryEntry for live message ingestion
- Add searchChatHistorySemantic for vector similarity search
- Update search_chat_history tool to use semantic search
- Add backfill command: run.sh Omni/Agent/Memory.hs backfill
- Add stats command: run.sh Omni/Agent/Memory.hs stats
- Change default memory.db path to ~/memory.db
- Wire Telegram message handling to save to chat_history async
Diffstat (limited to 'Omni/Agent/Tools/AvaLogs.hs')
| -rw-r--r-- | Omni/Agent/Tools/AvaLogs.hs | 65 |
1 files changed, 39 insertions, 26 deletions
diff --git a/Omni/Agent/Tools/AvaLogs.hs b/Omni/Agent/Tools/AvaLogs.hs index 84c9db8..4c8ce11 100644 --- a/Omni/Agent/Tools/AvaLogs.hs +++ b/Omni/Agent/Tools/AvaLogs.hs @@ -24,6 +24,7 @@ import qualified Data.Text as Text import qualified Data.Time as Time import qualified Omni.Agent.AuditLog as AuditLog import qualified Omni.Agent.Engine as Engine +import qualified Omni.Agent.Memory as Memory main :: IO () main = putText "Omni.Agent.Tools.AvaLogs - no standalone execution" @@ -114,9 +115,9 @@ searchChatHistoryTool = Engine.Tool { Engine.toolName = "search_chat_history", Engine.toolDescription = - "Search your conversation history for specific content. " + "Search your conversation history using semantic similarity. " <> "Use this to find what was said in past conversations, recall context, " - <> "or find when something was discussed. Searches message content.", + <> "or find when something was discussed. Finds semantically related messages.", Engine.toolJsonSchema = Aeson.object [ "type" .= ("object" :: Text), @@ -125,12 +126,7 @@ searchChatHistoryTool = [ "query" .= Aeson.object [ "type" .= ("string" :: Text), - "description" .= ("Text to search for in chat history" :: Text) - ], - "days_back" - .= Aeson.object - [ "type" .= ("integer" :: Text), - "description" .= ("How many days back to search (default: 7)" :: Text) + "description" .= ("What to search for (semantic search)" :: Text) ], "max_results" .= Aeson.object @@ -151,12 +147,6 @@ executeSearchHistory v = do _ -> "" _ -> "" - let daysBack = case v of - Aeson.Object obj -> case KeyMap.lookup "days_back" obj of - Just (Aeson.Number n) -> round n - _ -> 7 - _ -> 7 - let maxResults = case v of Aeson.Object obj -> case KeyMap.lookup "max_results" obj of Just (Aeson.Number n) -> round n @@ -166,18 +156,41 @@ executeSearchHistory v = do if Text.null query then pure <| Aeson.object ["error" .= ("query is required" :: Text)] else do - today <- Time.utctDay </ Time.getCurrentTime - let days = [Time.addDays (negate i) today | i <- [0 .. daysBack - 1]] - allEntries <- concat </ traverse AuditLog.readAvaLogs days - let matches = filter (matchesQuery query) allEntries - limited = take maxResults matches - pure - <| Aeson.object - [ "query" .= query, - "days_searched" .= daysBack, - "total_matches" .= length matches, - "results" .= map formatSearchResult limited - ] + (_total, indexed) <- Memory.getChatHistoryStats + if indexed > 0 + then do + results <- Memory.searchChatHistorySemantic query maxResults + pure + <| Aeson.object + [ "query" .= query, + "search_type" .= ("semantic" :: Text), + "indexed_messages" .= indexed, + "results" .= map formatSemanticResult results + ] + else do + today <- Time.utctDay </ Time.getCurrentTime + let days = [Time.addDays (negate i) today | i <- [0 .. 6]] + allEntries <- concat </ traverse AuditLog.readAvaLogs days + let matches = filter (matchesQuery query) allEntries + limited = take maxResults matches + pure + <| Aeson.object + [ "query" .= query, + "search_type" .= ("keyword_fallback" :: Text), + "note" .= ("no indexed history - run backfill" :: Text), + "total_matches" .= length matches, + "results" .= map formatSearchResult limited + ] + +formatSemanticResult :: (Memory.ChatHistoryEntry, Float) -> Aeson.Value +formatSemanticResult (entry, score) = + Aeson.object + [ "timestamp" .= Time.formatTime Time.defaultTimeLocale "%Y-%m-%d %H:%M:%S" (Memory.cheCreatedAt entry), + "role" .= Memory.cheRole entry, + "sender" .= Memory.cheSenderName entry, + "similarity" .= score, + "content" .= Text.take 500 (Memory.cheContent entry) + ] matchesQuery :: Text -> AuditLog.AuditLogEntry -> Bool matchesQuery query entry = |
