summaryrefslogtreecommitdiff
path: root/Omni/Ava.hs
blob: 60584251bba33082fc68940ee220a9e9d131863d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/env run.sh
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE NoImplicitPrelude #-}

-- | Ava - AI assistant via Telegram.
--
-- Usage:
--   ava                    # Uses TELEGRAM_BOT_TOKEN env var
--   ava --token=XXX        # Explicit token
--   ava --model=MODEL      # Override LLM model
--   ava logs [--last=N] [SUBAGENT_ID]  # View audit logs
--
-- : out ava
-- : dep aeson
-- : dep http-conduit
-- : dep http-types
-- : dep mustache
-- : dep stm
-- : dep time
-- : dep uuid
-- : dep wai
-- : dep warp
module Omni.Ava where

import Alpha
import qualified Control.Concurrent as Concurrent
import qualified Data.Aeson as Aeson
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import qualified Data.Time as Time
import qualified Omni.Agent.AuditLog as AuditLog
import qualified Omni.Agent.Telegram as Telegram
import qualified Omni.Ava.Web as Web
import qualified Omni.Cli as Cli
import qualified Omni.Test as Test
import qualified System.Console.Docopt as Docopt
import qualified System.Directory as Dir
import qualified System.Environment as Environment
import qualified System.IO as IO

main :: IO ()
main = Cli.main plan

plan :: Cli.Plan ()
plan =
  Cli.Plan
    { Cli.help = help,
      Cli.move = move,
      Cli.test = test,
      Cli.tidy = \_ -> pure ()
    }

help :: Cli.Docopt
help =
  [Cli.docopt|
ava - AI assistant via Telegram

Usage:
  ava [--token=TOKEN] [--model=MODEL]
  ava logs [--last=N] [<subagent_id>]
  ava test
  ava (-h | --help)

Options:
  -h --help         Show this help
  --token=TOKEN     Telegram bot token (or use TELEGRAM_BOT_TOKEN env)
  --model=MODEL     LLM model to use [default: anthropic/claude-sonnet-4]
  --last=N          Number of recent log entries to show [default: 50]
  <subagent_id>     Show logs for a specific subagent (e.g. S-abc123)
|]

move :: Cli.Arguments -> IO ()
move args = do
  IO.hSetBuffering IO.stdout IO.LineBuffering
  IO.hSetBuffering IO.stderr IO.LineBuffering
  if args `Cli.has` Cli.command "logs"
    then showLogs args
    else do
      webPort <- Environment.lookupEnv "AVA_WEB_PORT" /> maybe Web.defaultPort (fromMaybe Web.defaultPort <. readMaybe)
      dataRoot <- Environment.getEnv "AVA_DATA_ROOT"
      let dbPath = dataRoot <> "/ava.db"
      _ <- Concurrent.forkIO <| Web.startWebServer webPort dbPath
      let maybeToken = fmap Text.pack (Cli.getArg args (Cli.longOption "token"))
      Telegram.startBot maybeToken

showLogs :: Cli.Arguments -> IO ()
showLogs args = do
  let maybeSubagentId = Cli.getArg args (Cli.argument "subagent_id")
  let lastN = fromMaybe 50 (readMaybe =<< Cli.getArg args (Cli.longOption "last"))

  case maybeSubagentId of
    Just sidStr -> do
      let sid = AuditLog.SubagentId (Text.pack sidStr)
      let path = AuditLog.subagentLogPath sid
      exists <- Dir.doesFileExist path
      if exists
        then do
          entries <- AuditLog.readSubagentLogs sid
          putText <| "=== Subagent " <> Text.pack sidStr <> " (" <> tshow (length entries) <> " entries) ==="
          traverse_ printLogEntry entries
        else putText <| "No logs found for subagent: " <> Text.pack sidStr
    Nothing -> do
      entries <- AuditLog.getRecentAvaLogs lastN
      today <- Time.utctDay </ Time.getCurrentTime
      putText <| "=== Ava logs for " <> Text.pack (Time.formatTime Time.defaultTimeLocale "%Y-%m-%d" today) <> " (last " <> tshow lastN <> ") ==="
      traverse_ printLogEntry entries

printLogEntry :: AuditLog.AuditLogEntry -> IO ()
printLogEntry entry = do
  let ts = Text.pack <| Time.formatTime Time.defaultTimeLocale "%H:%M:%S" (AuditLog.logTimestamp entry)
  let evType = tshow (AuditLog.logEventType entry)
  let agent = AuditLog.unAgentId (AuditLog.logAgentId entry)
  let content = case AuditLog.logContent entry of
        Aeson.String t -> Text.take 100 t
        Aeson.Object _ -> "<object>"
        _ -> "<value>"
  TextIO.putStrLn <| "[" <> ts <> "] " <> agent <> " | " <> evType <> ": " <> content

test :: Test.Tree
test =
  Test.group
    "Omni.Ava"
    [ Test.unit "help is non-empty" <| do
        let usage = str (Docopt.usage help) :: String
        null usage Test.@=? False
    ]