packages feed

shikumi-tools-0.4.0.0: src/Shikumi/Tool/Output.hs

-- | Lossless local tool results and explicit provider/text projections.
module Shikumi.Tool.Output
  ( ToolOutput (..),
    textToolOutput,
    renderToolOutput,
    toolOutputMessage,
  )
where

import Baikai qualified as B
import Control.Lens ((%~), (&), (^.))
import Data.Aeson (Value, encode, toJSON)
import Data.ByteString.Lazy qualified as LBS
import Data.Generics.Labels ()
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding (decodeUtf8)
import Data.Vector qualified as V

-- | Native content, optional structured data, and uninterpreted extension blocks.
-- Image preservation does not imply that a provider accepts image tool results.
data ToolOutput = ToolOutput
  { result :: !B.ToolResult,
    structuredContent :: !(Maybe Value),
    extensionBlocks :: ![Value]
  }
  deriving stock (Eq, Show)

textToolOutput :: Text -> ToolOutput
textToolOutput t = ToolOutput (B.toolResultText t) Nothing []

jsonText :: Value -> Text
jsonText = decodeUtf8 . LBS.toStrict . encode

extraText :: ToolOutput -> [Text]
extraText o =
  maybe [] (\v -> ["Structured JSON: " <> jsonText v]) (structuredContent o)
    <> map (("Extension JSON: " <>) . jsonText) (extensionBlocks o)

-- | Text projection preserves image bytes as labeled JSON rather than dropping them.
renderToolOutput :: ToolOutput -> Text
renderToolOutput o = (if result o ^. #isError then "Tool error: " else "") <> T.intercalate "\n" (map render (V.toList (result o ^. #content)) <> extraText o)
  where
    render (B.ToolResultText (B.TextContent t)) = t
    render (B.ToolResultImage img) = "Image JSON: " <> jsonText (toJSON img)

-- | Preserve native blocks and append explicitly labeled JSON text blocks.
toolOutputMessage :: B.ToolCall -> ToolOutput -> B.Message
toolOutputMessage call o =
  B.toolResultFromCall
    call
    (result o & #content %~ (<> V.fromList (map (B.ToolResultText . B.TextContent) (extraText o))))