agentic-0.2.0.0: src/Agentic/ViaLLM.hs
-- | Letting an LLM stand in as System One, when there's no Jev.
module Agentic.ViaLLM
( viaLLM
) where
import Agentic.Core (Instruction (..))
import Agentic.Questions
import Agentic.Runtime
import Agentic.Schema
import Agentic.Value (Value (..), lookupField, renderJson)
import Data.List (maximumBy)
import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T
-- | Answer judgements with one LLM turn per request. The LLM gives a
-- probability for each answer; unlike Jev's, they aren't calibrated.
viaLLM :: MonadFail m => SystemTwo m -> SystemOne m
viaLLM two = SystemOne $ \request -> do
let qs = zip ids (requestQuestions request)
conversation =
Conversation
{ path = []
, instruction = Instruction "Answer each question about the input. Give every probability as a number from 0 to 1."
, state = requestState request
, stateSchema = schemaOf SNull
, tools = []
, output = schemaOf (SObject [Field qid (questionSchema q) True | (qid, q) <- qs])
, history = []
}
turn <- askSystemTwo two conversation
case action turn of
Respond (Object kvs) -> either (fail . T.unpack) pure (traverse (\(qid, q) -> answer q =<< field' qid kvs) qs)
Respond other -> fail ("viaLLM: expected an object of answers, got " <> T.unpack (renderJson other))
CallTools _ -> fail "viaLLM: the model called a tool while answering questions"
where
ids = ["q" <> T.pack (show n) | n <- [0 :: Int ..]]
field' k kvs = maybe (Left ("missing " <> k)) Right (lookupField k kvs)
questionSchema :: QuestionSpec -> Schema
questionSchema = \case
AskYesNo q ->
documentSchema q (schemaOf (SObject [Field "probabilityYes" (documentSchema "The probability that the answer is yes" (schemaOf SNumber)) True]))
AskChoice q opts -> distribution (q <> " Give each option's probability; they should sum to 1.") opts
AskScore q levels -> distribution (q <> " The options are ordered levels, lowest first. Give each level's probability; they should sum to 1.") levels
where
distribution q opts =
documentSchema q (schemaOf (SObject [Field l (documentSchema (maybe l id d) (schemaOf SNumber)) True | (l, d) <- opts]))
answer :: QuestionSpec -> Value -> Either Text Answer
answer spec v = case (spec, v) of
(AskYesNo _, Object kvs) -> YesNoAnswer <$> (number =<< get "probabilityYes" kvs)
(AskChoice _ opts, Object kvs) -> do
ps <- traverse (\(l, _) -> (l,) <$> (number =<< get l kvs)) opts
let (best, p) = maximumBy (comparing snd) ps
pure (ChoiceAnswer best ps p)
(AskScore _ levels, Object kvs) -> do
ps <- traverse (\(l, _) -> number =<< get l kvs) levels
let weights = map probability ps
total = sum weights
pos = if total > 0 then sum (zipWith (*) [0 ..] weights) / total else 0
pure (ScoreAnswer pos (zip [0 ..] ps) (maximum ps))
(_, other) -> Left ("expected an object, got " <> renderJson other)
where
get k kvs = maybe (Left ("missing " <> k)) Right (lookupField k kvs)
number = \case
Number d -> Right (fromBasisPoints d)
Integer n -> Right (fromBasisPoints (fromInteger n))
other -> Left ("expected a probability, got " <> renderJson other)