diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-19 09:54:09 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-19 09:54:09 -0500 |
| commit | 533e4209192298de4808c58f6ea6244e4bed5768 (patch) | |
| tree | 1cd1d695af4b0f914250ea4f2004fdd4f0c00a5b /Omni/Agent | |
| parent | 59f4af68ff678db2349d8a9f40fd24b943131439 (diff) | |
Add search_chat_history tool for Ava
Allows Ava to search her conversation logs for past discussions.
Searches UserMessage/AssistantMessage events with case-insensitive
matching, configurable days_back (default 7) and max_results (default 20).
Diffstat (limited to 'Omni/Agent')
| -rw-r--r-- | Omni/Agent/Telegram.hs | 1 | ||||
| -rw-r--r-- | Omni/Agent/Tools/AvaLogs.hs | 105 |
2 files changed, 106 insertions, 0 deletions
diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs index 913fc2b..e59570a 100644 --- a/Omni/Agent/Telegram.hs +++ b/Omni/Agent/Telegram.hs @@ -1179,6 +1179,7 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe else [] auditLogTools = [AvaLogs.readAvaLogsTool | isBenAuthorized userName] + <> [AvaLogs.searchChatHistoryTool] tools = memoryTools <> searchTools <> webReaderTools <> pdfTools <> notesTools <> calendarTools <> todoTools <> messageTools <> hledgerTools <> emailTools <> pythonTools <> httpTools <> outreachTools <> feedbackTools <> fileTools <> skillsTools <> subagentToolList <> auditLogTools let agentCfg = diff --git a/Omni/Agent/Tools/AvaLogs.hs b/Omni/Agent/Tools/AvaLogs.hs index 582b3a6..84c9db8 100644 --- a/Omni/Agent/Tools/AvaLogs.hs +++ b/Omni/Agent/Tools/AvaLogs.hs @@ -10,6 +10,7 @@ -- : dep time module Omni.Agent.Tools.AvaLogs ( readAvaLogsTool, + searchChatHistoryTool, main, ) where @@ -107,3 +108,107 @@ summarizeContent (Aeson.Array arr) = "array with " <> tshow (length arr) <> " it summarizeContent Aeson.Null = "null" summarizeContent (Aeson.Bool b) = if b then "true" else "false" summarizeContent (Aeson.Number n) = tshow n + +searchChatHistoryTool :: Engine.Tool +searchChatHistoryTool = + Engine.Tool + { Engine.toolName = "search_chat_history", + Engine.toolDescription = + "Search your conversation history for specific content. " + <> "Use this to find what was said in past conversations, recall context, " + <> "or find when something was discussed. Searches message content.", + Engine.toolJsonSchema = + Aeson.object + [ "type" .= ("object" :: Text), + "properties" + .= Aeson.object + [ "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) + ], + "max_results" + .= Aeson.object + [ "type" .= ("integer" :: Text), + "description" .= ("Maximum results to return (default: 20)" :: Text) + ] + ], + "required" .= (["query"] :: [Text]) + ], + Engine.toolExecute = executeSearchHistory + } + +executeSearchHistory :: Aeson.Value -> IO Aeson.Value +executeSearchHistory v = do + let query = case v of + Aeson.Object obj -> case KeyMap.lookup "query" obj of + Just (Aeson.String q) -> q + _ -> "" + _ -> "" + + 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 + _ -> 20 + _ -> 20 + + 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 + ] + +matchesQuery :: Text -> AuditLog.AuditLogEntry -> Bool +matchesQuery query entry = + let eventType = AuditLog.logEventType entry + isMessage = eventType == AuditLog.UserMessage || eventType == AuditLog.AssistantMessage + content = extractContent (AuditLog.logContent entry) + queryLower = Text.toLower query + in isMessage && queryLower `Text.isInfixOf` Text.toLower content + +extractContent :: Aeson.Value -> Text +extractContent (Aeson.String t) = t +extractContent (Aeson.Object obj) = + case KeyMap.lookup "text" obj of + Just (Aeson.String t) -> t + _ -> case KeyMap.lookup "content" obj of + Just (Aeson.String t) -> t + _ -> case KeyMap.lookup "message" obj of + Just (Aeson.String t) -> t + _ -> "" +extractContent _ = "" + +formatSearchResult :: AuditLog.AuditLogEntry -> Aeson.Value +formatSearchResult entry = + Aeson.object + [ "timestamp" .= Time.formatTime Time.defaultTimeLocale "%Y-%m-%d %H:%M:%S" (AuditLog.logTimestamp entry), + "role" .= roleText (AuditLog.logEventType entry), + "user" .= AuditLog.logUserId entry, + "content" .= Text.take 500 (extractContent (AuditLog.logContent entry)) + ] + +roleText :: AuditLog.AuditEventType -> Text +roleText AuditLog.UserMessage = "user" +roleText AuditLog.AssistantMessage = "assistant" +roleText _ = "other" |
