summaryrefslogtreecommitdiff
path: root/Omni/Agent
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-19 09:54:09 -0500
committerBen Sima <ben@bensima.com>2025-12-19 09:54:09 -0500
commit533e4209192298de4808c58f6ea6244e4bed5768 (patch)
tree1cd1d695af4b0f914250ea4f2004fdd4f0c00a5b /Omni/Agent
parent59f4af68ff678db2349d8a9f40fd24b943131439 (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.hs1
-rw-r--r--Omni/Agent/Tools/AvaLogs.hs105
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"