packages feed

shikumi-optimize-0.2.0.0: src/Shikumi/Optimize/Propose/Summarize.hs

{-# LANGUAGE FlexibleContexts #-}

-- | The signal-gatherers of the grounded proposer (EP-19) that are themselves typed
-- Shikumi 'Program's — preserving V1's "the optimizer is written in the framework it
-- optimizes" pattern. Each mirrors a DSPy proposer sub-module:
--
--   * 'renderProgramPseudo' / 'programDescriber' — DSPy's @DescribeProgram@: render
--     the whole program as deterministic pseudo-code and describe what it does.
--   * 'datasetDescriber' / 'observationSummarizer' / 'datasetSummary' — DSPy's
--     @DatasetDescriptor@ + @ObservationSummarizer@: observe patterns across sampled
--     rows, then condense them into a 2-3 sentence summary.
--   * 'moduleDescriber' — DSPy's @DescribeModule@: describe one node's role within
--     the broader program.
module Shikumi.Optimize.Propose.Summarize
  ( -- * Program pseudo-code
    renderProgramPseudo,

    -- * Row rendering (shared with the grounded driver)
    renderExampleRow,
    encodeJsonText,

    -- * The program describer
    ProgramDescribeIn (..),
    ProgramDescribeOut (..),
    programDescriber,

    -- * The dataset summarizer
    DatasetDescribeIn (..),
    DatasetDescribeOut (..),
    datasetDescriber,
    SummarizeIn (..),
    SummarizeOut (..),
    observationSummarizer,
    datasetSummary,

    -- * The module describer
    ModuleDescribeIn (..),
    ModuleDescribeOut (..),
    moduleDescriber,
  )
where

import Data.Aeson (ToJSON)
import Data.Aeson.Text (encodeToLazyText)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Effectful (Eff, (:>))
import Effectful.Error.Static (Error)
import GHC.Generics (Generic)
import Shikumi.Adapter (ToPrompt)
import Shikumi.Error (ShikumiError)
import Shikumi.Eval (Dataset, Example (..), datasetExamples)
import Shikumi.LLM (LLM)
import Shikumi.Module (predict)
import Shikumi.Optimize.Propose.Types (NodeFieldNames (..), programFieldNames)
import Shikumi.Program
  ( Program,
    ProgramShape (..),
    programShape,
    runProgram,
  )
import Shikumi.Schema (FromModel, ToSchema, Validatable)
import Shikumi.Signature (mkSignature)

-- ---------------------------------------------------------------------------
-- Program pseudo-code
-- ---------------------------------------------------------------------------

-- | Render a program as a short, deterministic, human-readable outline: one line
-- per node and combinator, with 'Predict' nodes shown as @predict(inputs) ->
-- outputs@ (using 'programFieldNames') and combinators shown by name. Shikumi's
-- analogue of DSPy's @get_dspy_source_code@. Deterministic, so tests can assert on it.
renderProgramPseudo :: Program i o -> Text
renderProgramPseudo prog = T.intercalate "\n" (fst (go (programShape prog) (programFieldNames prog)))
  where
    go :: ProgramShape -> [NodeFieldNames] -> ([Text], [NodeFieldNames])
    go (ShapePredict lbl) nfs = case nfs of
      (nf : rest) ->
        (["predict(" <> commas (inputFieldNames nf) <> ") -> " <> commas (outputFieldNames nf)], rest)
      [] -> (["predict(?) -> " <> lbl], [])
    go (ShapeCompose a b) nfs = combine "compose" [a, b] nfs
    go (ShapeFMap a) nfs = combine "fmap" [a] nfs
    go (ShapeMap _ a) nfs = combine "map" [a] nfs
    go (ShapeParallel a b) nfs = combine "parallel" [a, b] nfs
    go (ShapeRetry _ a) nfs = combine "retry" [a] nfs
    go (ShapeRetryWhen _ a) nfs = combine "retryWhen" [a] nfs
    go (ShapeValidate a) nfs = combine "validate" [a] nfs
    go (ShapeMajorityVote _ _ a) nfs = combine "majorityVote" [a] nfs
    go (ShapeEnsemble as) nfs = combine "ensemble" as nfs
    go ShapeEmbed nfs = (["embed"], nfs)
    combine name children nfs0 =
      let (lns, rest) = foldl step ([], nfs0) children
          step (acc, nf) c = let (l, nf') = go c nf in (acc ++ l, nf')
       in ((name <> ":") : map ("  " <>) lns, rest)
    commas xs = if null xs then "?" else T.intercalate ", " xs

-- | Render a single dataset example as @<input-json> => <expected-json>@.
renderExampleRow :: (ToJSON i, ToJSON o) => Example i o -> Text
renderExampleRow (Example i o) = encodeJsonText i <> " => " <> encodeJsonText o

-- | Encode any 'ToJSON' value to compact 'Text'.
encodeJsonText :: (ToJSON a) => a -> Text
encodeJsonText = TL.toStrict . encodeToLazyText

-- ---------------------------------------------------------------------------
-- The program describer
-- ---------------------------------------------------------------------------

data ProgramDescribeIn = ProgramDescribeIn
  { programCode :: !Text,
    programExample :: !Text
  }
  deriving stock (Generic, Show)

instance FromModel ProgramDescribeIn

instance ToPrompt ProgramDescribeIn

newtype ProgramDescribeOut = ProgramDescribeOut {programDescription :: Text}
  deriving stock (Generic, Show)

instance ToSchema ProgramDescribeOut

instance FromModel ProgramDescribeOut

instance ToPrompt ProgramDescribeOut

instance Validatable ProgramDescribeOut

programDescriber :: Program ProgramDescribeIn ProgramDescribeOut
programDescriber =
  predict
    ( mkSignature
        "Below is pseudo-code for a pipeline that solves a task with calls to a language \
        \model. Describe, in two or three sentences, what task this program is designed to \
        \solve and how it goes about solving it. Write your answer in the `programDescription` field."
    )

-- ---------------------------------------------------------------------------
-- The dataset summarizer
-- ---------------------------------------------------------------------------

newtype DatasetDescribeIn = DatasetDescribeIn {examples :: Text}
  deriving stock (Generic, Show)

instance FromModel DatasetDescribeIn

instance ToPrompt DatasetDescribeIn

newtype DatasetDescribeOut = DatasetDescribeOut {observations :: Text}
  deriving stock (Generic, Show)

instance ToSchema DatasetDescribeOut

instance FromModel DatasetDescribeOut

instance ToPrompt DatasetDescribeOut

instance Validatable DatasetDescribeOut

datasetDescriber :: Program DatasetDescribeIn DatasetDescribeOut
datasetDescriber =
  predict
    ( mkSignature
        "Given several examples from a dataset, write observations about trends that hold for \
        \most or all of the samples — topics, content, syntax, brevity — and guess at the task \
        \the dataset enables. Write them in the `observations` field."
    )

newtype SummarizeIn = SummarizeIn {observations :: Text}
  deriving stock (Generic, Show)

instance FromModel SummarizeIn

instance ToPrompt SummarizeIn

newtype SummarizeOut = SummarizeOut {summary :: Text}
  deriving stock (Generic, Show)

instance ToSchema SummarizeOut

instance FromModel SummarizeOut

instance ToPrompt SummarizeOut

instance Validatable SummarizeOut

observationSummarizer :: Program SummarizeIn SummarizeOut
observationSummarizer =
  predict
    ( mkSignature
        "Condense the dataset notes below into a brief two-to-three sentence summary highlighting \
        \only the most important details. Write it in the `summary` field."
    )

-- | Sample up to @viewBatch@ rows, render them, observe trends, then condense into a
-- short summary. A single observation batch (DSPy's batched refinement loop is noted
-- as a future enhancement); the goal here is that the summary signal /reaches/ the
-- proposer.
datasetSummary ::
  (ToJSON i, ToJSON o, LLM :> es, Error ShikumiError :> es) =>
  -- | viewBatch: how many rows to sample
  Int ->
  Dataset i o ->
  Eff es Text
datasetSummary viewBatch ds = do
  let rendered = T.intercalate "\n" (map renderExampleRow (take (max 1 viewBatch) (datasetExamples ds)))
  DatasetDescribeOut obs <- runProgram datasetDescriber (DatasetDescribeIn rendered)
  SummarizeOut s <- runProgram observationSummarizer (SummarizeIn obs)
  pure s

-- ---------------------------------------------------------------------------
-- The module describer
-- ---------------------------------------------------------------------------

data ModuleDescribeIn = ModuleDescribeIn
  { programCode :: !Text,
    programDescription :: !Text,
    moduleSignature :: !Text
  }
  deriving stock (Generic, Show)

instance FromModel ModuleDescribeIn

instance ToPrompt ModuleDescribeIn

newtype ModuleDescribeOut = ModuleDescribeOut {moduleDescription :: Text}
  deriving stock (Generic, Show)

instance ToSchema ModuleDescribeOut

instance FromModel ModuleDescribeOut

instance ToPrompt ModuleDescribeOut

instance Validatable ModuleDescribeOut

moduleDescriber :: Program ModuleDescribeIn ModuleDescribeOut
moduleDescriber =
  predict
    ( mkSignature
        "Below is pseudo-code for a language-model pipeline and a description of what it does. \
        \Describe the role of the one specified module within the broader program. Write it in \
        \the `moduleDescription` field."
    )