summaryrefslogtreecommitdiff
path: root/Omni/Agent/Telegram.hs
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-14 22:45:09 -0500
committerBen Sima <ben@bensima.com>2025-12-14 22:45:09 -0500
commit8c07a16dd9a7a3ad1847d0c665265e98f7df5438 (patch)
tree983ec8406dc738315d6645dde6f48e898e11283b /Omni/Agent/Telegram.hs
parent89d9fc7449ab2e799742470c3294c6e062e6de0b (diff)
Add python_exec tool for agent Python execution
- Create Omni/Agent/Tools/Python.hs with python_exec tool - Execute Python snippets via subprocess with 30s default timeout - Return structured JSON with stdout, stderr, exit_code - Add 8 unit tests covering print, imports, errors, timeout - Wire tool into Telegram agent's tool list Completes t-265.1
Diffstat (limited to 'Omni/Agent/Telegram.hs')
-rw-r--r--Omni/Agent/Telegram.hs57
1 files changed, 55 insertions, 2 deletions
diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs
index 6da1484..2f0a029 100644
--- a/Omni/Agent/Telegram.hs
+++ b/Omni/Agent/Telegram.hs
@@ -92,6 +92,7 @@ import qualified Omni.Agent.Tools.Email as Email
import qualified Omni.Agent.Tools.Hledger as Hledger
import qualified Omni.Agent.Tools.Notes as Notes
import qualified Omni.Agent.Tools.Pdf as Pdf
+import qualified Omni.Agent.Tools.Python as Python
import qualified Omni.Agent.Tools.Todos as Todos
import qualified Omni.Agent.Tools.WebReader as WebReader
import qualified Omni.Agent.Tools.WebSearch as WebSearch
@@ -959,7 +960,8 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe
if isEmailAuthorized userName
then Email.allEmailTools
else []
- tools = memoryTools <> searchTools <> webReaderTools <> pdfTools <> notesTools <> calendarTools <> todoTools <> messageTools <> hledgerTools <> emailTools
+ pythonTools = [Python.pythonExecTool]
+ tools = memoryTools <> searchTools <> webReaderTools <> pdfTools <> notesTools <> calendarTools <> todoTools <> messageTools <> hledgerTools <> emailTools <> pythonTools
let agentCfg =
Engine.defaultAgentConfig
@@ -1000,7 +1002,9 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe
_ <- Messages.enqueueImmediate (Just uid) chatId threadId "hmm, i don't have a response for that" (Just "agent_response") Nothing
pure ()
else do
- _ <- Messages.enqueueImmediate (Just uid) chatId threadId response (Just "agent_response") Nothing
+ parts <- splitMessageForChat (Types.tgOpenRouterApiKey tgConfig) response
+ putText <| "Split response into " <> tshow (length parts) <> " parts"
+ enqueueMultipart (Just uid) chatId threadId parts (Just "agent_response")
unless isGroup <| checkAndSummarize (Types.tgOpenRouterApiKey tgConfig) uid chatId
let cost = Engine.resultTotalCost agentResult
costStr = Text.pack (printf "%.2f" cost)
@@ -1053,6 +1057,55 @@ checkAndSummarize openRouterKey uid chatId = do
_ <- Memory.summarizeAndArchive uid chatId summary
putText "Conversation summarized and archived (gemini)"
+splitMessageForChat :: Text -> Text -> IO [Text]
+splitMessageForChat openRouterKey message = do
+ if Text.length message < 200
+ then pure [message]
+ else do
+ let haiku = Provider.defaultOpenRouter openRouterKey "anthropic/claude-haiku-4.5"
+ result <-
+ Provider.chat
+ haiku
+ []
+ [ Provider.Message
+ Provider.System
+ ( Text.unlines
+ [ "Split this message into separate chat messages that feel natural in a messaging app.",
+ "Each part should be logically independent - a complete thought.",
+ "Separate parts with exactly '---' on its own line.",
+ "Keep the original text, just add separators. Don't add any commentary.",
+ "If the message is already short/simple, return it unchanged (no separators).",
+ "Aim for 2-4 parts maximum. Don't over-split.",
+ "",
+ "Good splits: between topics, after questions, between a statement and follow-up",
+ "Bad splits: mid-sentence, between closely related points"
+ ]
+ )
+ Nothing
+ Nothing,
+ Provider.Message Provider.User message Nothing Nothing
+ ]
+ case result of
+ Left err -> do
+ putText <| "Message split failed: " <> err
+ pure [message]
+ Right msg -> do
+ let parts = map Text.strip (Text.splitOn "---" (Provider.msgContent msg))
+ validParts = filter (not <. Text.null) parts
+ if null validParts
+ then pure [message]
+ else pure validParts
+
+enqueueMultipart :: Maybe Text -> Int -> Maybe Int -> [Text] -> Maybe Text -> IO ()
+enqueueMultipart _ _ _ [] _ = pure ()
+enqueueMultipart mUid chatId mThreadId parts msgType = do
+ forM_ (zip [0 ..] parts) <| \(i :: Int, part) -> do
+ if i == 0
+ then void <| Messages.enqueueImmediate mUid chatId mThreadId part msgType Nothing
+ else do
+ let delaySeconds = fromIntegral (i * 2)
+ void <| Messages.enqueueDelayed mUid chatId mThreadId part delaySeconds msgType Nothing
+
shouldEngageInGroup :: Text -> Text -> IO Bool
shouldEngageInGroup openRouterKey messageText = do
let gemini = Provider.defaultOpenRouter openRouterKey "google/gemini-2.0-flash-001"