packages feed

shikumi-optimize-0.1.0.0: src/Shikumi/Optimize/Propose/Types.hs

-- | The shared types of the grounded instruction proposer (EP-19): the per-node
-- field-metadata accessor (integration point #3), the instruction-history vocabulary,
-- and the proposer's request/result records.
--
-- This is the contract MIPROv2 (@docs/plans/20-miprov2-optimizer.md@) and COPRO
-- (@docs/plans/21-copro-instruction-optimizer.md@) both consume, so it lives in its
-- own module — neither optimizer drags in @instructionSearch@'s loop to use it.
module Shikumi.Optimize.Propose.Types
  ( -- * Per-node field metadata (integration point #3)
    NodeFieldNames (..),
    programFieldNames,
    moduleSignatureAt,

    -- * Instruction history
    PastInstruction (..),
    renderHistory,

    -- * The proposer request/result
    ProposeRequest (..),
    ProposeResult (..),
  )
where

import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Shikumi.Program (NodeFields (NodeFields), Program, nodeFieldsIndexed)

-- | A node's input/output field names, recovered structurally. A 'Predict' node
-- hides its @i@/@o@ types existentially, so this carries the field /names/ (plain
-- 'Text'), never a typed @Signature@ — exactly what EP-16's @nodeFieldsIndexed@
-- returns.
data NodeFieldNames = NodeFieldNames
  { inputFieldNames :: ![Text],
    outputFieldNames :: ![Text]
  }
  deriving stock (Eq, Show, Generic)

-- | One 'NodeFieldNames' per 'Predict' node, in @foldParams@/@mapParamsAt@ order
-- (integration point #3). Delegates to EP-16's @nodeFieldsIndexed@; the count and
-- ordering align with @foldParams@ by construction, so
-- @programFieldNames prog !! k@ describes the node @mapParamsAt k@ edits.
programFieldNames :: Program i o -> [NodeFieldNames]
programFieldNames = map convert . nodeFieldsIndexed
  where
    convert (NodeFields i o) = NodeFieldNames i o

-- | A human-readable signature for node @k@: @predict(in1, in2) -> out1@. Used as
-- the @moduleSignature@ signal in a proposal prompt. An out-of-range index yields
-- @predict(?) -> ?@.
moduleSignatureAt :: Int -> Program i o -> Text
moduleSignatureAt k prog = case drop k (programFieldNames prog) of
  (nf : _) -> "predict(" <> commas (inputFieldNames nf) <> ") -> " <> commas (outputFieldNames nf)
  [] -> "predict(?) -> ?"
  where
    commas xs = if null xs then "?" else T.intercalate ", " xs

-- | One past attempt for a node: the instruction text and the score it earned.
data PastInstruction = PastInstruction
  { pastInstruction :: !Text,
    pastScore :: !Double
  }
  deriving stock (Eq, Show, Generic)

-- | Render the instruction history as lines @"score 0.83 :: <instruction>"@, capped
-- at @maxInHistory@ entries. Empty history renders as @"No previous instructions."@
-- so the proposal prompt is always well-formed.
renderHistory :: Int -> [PastInstruction] -> Text
renderHistory maxInHistory hist = case take (max 0 maxInHistory) hist of
  [] -> "No previous instructions."
  xs -> T.intercalate "\n" (map line xs)
  where
    line (PastInstruction ins sc) = "score " <> tshow sc <> " :: " <> ins

-- | Everything the grounded proposer needs to propose instructions for one node.
data ProposeRequest i o = ProposeRequest
  { -- | the program being optimized
    program :: !(Program i o),
    -- | @foldParams@ index of the node to propose for
    targetNode :: !Int,
    -- | the node's instruction now (always retained as a candidate)
    currentInstruction :: !Text,
    -- | prior attempts + scores for this node
    history :: ![PastInstruction],
    -- | rendered demos for this node (may be empty)
    bootstrappedDemos :: ![Text],
    -- | how many fresh candidates to ask for (N)
    numCandidates :: !Int,
    -- | selects a tip deterministically (added to the per-candidate offset)
    tipIndex :: !Int,
    -- | dataset rows to sample for the summary
    viewBatch :: !Int
  }

-- | A ranked list of candidate instruction strings for one node, distinct and with
-- the current instruction always present (and first). The proposer does not score —
-- scoring belongs to the calling optimizer.
newtype ProposeResult = ProposeResult {rankedCandidates :: [Text]}
  deriving stock (Eq, Show, Generic)

tshow :: (Show a) => a -> Text
tshow = T.pack . show