{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NoImplicitPrelude #-} -- | Feedback query tool for PodcastItLater user research. -- -- Allows the agent to query collected feedback from the PIL database. -- Feedback is submitted via /feedback on the PIL web app. -- -- : out omni-agent-tools-feedback -- : dep aeson -- : dep http-conduit module Omni.Agent.Tools.Feedback ( -- * Tools feedbackListTool, allFeedbackTools, -- * Types FeedbackEntry (..), ListFeedbackArgs (..), -- * Testing main, test, ) where import Alpha import Data.Aeson ((.!=), (.:), (.:?), (.=)) import qualified Data.Aeson as Aeson import qualified Data.Text as Text import qualified Network.HTTP.Simple as HTTP import qualified Omni.Agent.Engine as Engine import qualified Omni.Test as Test import System.Environment (lookupEnv) main :: IO () main = Test.run test test :: Test.Tree test = Test.group "Omni.Agent.Tools.Feedback" [ Test.unit "feedbackListTool has correct name" <| do Engine.toolName feedbackListTool Test.@=? "feedback_list", Test.unit "allFeedbackTools has 1 tool" <| do length allFeedbackTools Test.@=? 1, Test.unit "ListFeedbackArgs parses correctly" <| do let json = Aeson.object ["limit" .= (10 :: Int)] case Aeson.fromJSON json of Aeson.Success (args :: ListFeedbackArgs) -> lfaLimit args Test.@=? 10 Aeson.Error e -> Test.assertFailure e, Test.unit "ListFeedbackArgs parses with since" <| do let json = Aeson.object [ "limit" .= (20 :: Int), "since" .= ("2024-01-01" :: Text) ] case Aeson.fromJSON json of Aeson.Success (args :: ListFeedbackArgs) -> do lfaLimit args Test.@=? 20 lfaSince args Test.@=? Just "2024-01-01" Aeson.Error e -> Test.assertFailure e, Test.unit "FeedbackEntry JSON roundtrip" <| do let entry = FeedbackEntry { feId = "abc123", feEmail = Just "test@example.com", feSource = Just "outreach", feCampaignId = Nothing, feRating = Just 4, feFeedbackText = Just "Great product!", feUseCase = Just "Commute listening", feCreatedAt = "2024-01-15T10:00:00Z" } case Aeson.decode (Aeson.encode entry) of Nothing -> Test.assertFailure "Failed to decode FeedbackEntry" Just decoded -> do feId decoded Test.@=? "abc123" feEmail decoded Test.@=? Just "test@example.com" feRating decoded Test.@=? Just 4 ] data FeedbackEntry = FeedbackEntry { feId :: Text, feEmail :: Maybe Text, feSource :: Maybe Text, feCampaignId :: Maybe Text, feRating :: Maybe Int, feFeedbackText :: Maybe Text, feUseCase :: Maybe Text, feCreatedAt :: Text } deriving (Show, Eq, Generic) instance Aeson.ToJSON FeedbackEntry where toJSON e = Aeson.object [ "id" .= feId e, "email" .= feEmail e, "source" .= feSource e, "campaign_id" .= feCampaignId e, "rating" .= feRating e, "feedback_text" .= feFeedbackText e, "use_case" .= feUseCase e, "created_at" .= feCreatedAt e ] instance Aeson.FromJSON FeedbackEntry where parseJSON = Aeson.withObject "FeedbackEntry" <| \v -> (FeedbackEntry (v .:? "email") <*> (v .:? "source") <*> (v .:? "campaign_id") <*> (v .:? "rating") <*> (v .:? "feedback_text") <*> (v .:? "use_case") <*> (v .: "created_at") data ListFeedbackArgs = ListFeedbackArgs { lfaLimit :: Int, lfaSince :: Maybe Text } deriving (Show, Eq, Generic) instance Aeson.FromJSON ListFeedbackArgs where parseJSON = Aeson.withObject "ListFeedbackArgs" <| \v -> (ListFeedbackArgs (v .:? "since") allFeedbackTools :: [Engine.Tool] allFeedbackTools = [feedbackListTool] feedbackListTool :: Engine.Tool feedbackListTool = Engine.Tool { Engine.toolName = "feedback_list", Engine.toolDescription = "List feedback entries from PodcastItLater users. " <> "Use to review user research data and understand what potential " <> "customers want from the product.", Engine.toolJsonSchema = Aeson.object [ "type" .= ("object" :: Text), "properties" .= Aeson.object [ "limit" .= Aeson.object [ "type" .= ("integer" :: Text), "description" .= ("Max entries to return (default: 20)" :: Text) ], "since" .= Aeson.object [ "type" .= ("string" :: Text), "description" .= ("ISO date to filter by (entries after this date)" :: Text) ] ], "required" .= ([] :: [Text]) ], Engine.toolExecute = executeFeedbackList } executeFeedbackList :: Aeson.Value -> IO Aeson.Value executeFeedbackList v = case Aeson.fromJSON v of Aeson.Error e -> pure (Aeson.object ["error" .= Text.pack e]) Aeson.Success (args :: ListFeedbackArgs) -> do mBaseUrl <- lookupEnv "PIL_BASE_URL" let baseUrl = maybe "http://localhost:8000" Text.pack mBaseUrl limit = min 100 (max 1 (lfaLimit args)) sinceParam = case lfaSince args of Nothing -> "" Just since -> "&since=" <> since url = baseUrl <> "/api/feedback?limit=" <> tshow limit <> sinceParam result <- fetchFeedback url case result of Left err -> pure (Aeson.object ["error" .= err]) Right entries -> pure ( Aeson.object [ "success" .= True, "count" .= length entries, "entries" .= entries ] ) fetchFeedback :: Text -> IO (Either Text [FeedbackEntry]) fetchFeedback url = do result <- try <| do req <- HTTP.parseRequest (Text.unpack url) resp <- HTTP.httpLBS req pure (HTTP.getResponseStatusCode resp, HTTP.getResponseBody resp) case result of Left (e :: SomeException) -> pure (Left ("Request failed: " <> tshow e)) Right (status, body) -> if status /= 200 then pure (Left ("HTTP " <> tshow status)) else case Aeson.decode body of Nothing -> pure (Left "Failed to parse response") Just entries -> pure (Right entries)