packages feed

shikumi-compile-0.1.0.0: src/Shikumi/Compile/ChainOfThought.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | The chain-of-thought compiler: ensure every LM-call node reasons step by step
-- before answering.
--
-- __Implementation: a structural rewrite (the plan's preferred form).__ EP-4 has
-- no @reasoning@ flag on 'Shikumi.Program.Params' to flip; instead, reasoning is a
-- /structural/ property — 'Shikumi.Module.chainOfThought' augments a signature's
-- output with a leading @reasoning@ field and amends its instruction to "think step
-- by step", then projects the answer back out with @FMap@. So this compiler walks
-- the program and replaces each @Predict sig ps@ leaf with exactly that shape:
-- @FMap value (chainOfThoughtRaw sig)@, carrying the node's existing 'Params'
-- across. The rewrite is type-preserving (a @Program i o@ stays a @Program i o@)
-- because the augmented @Program i (WithReasoning o)@ is mapped back through
-- 'Shikumi.Module.value'.
--
-- This is feasible — and faithful — only because EP-4 /exports the GADT
-- constructors/, so a downstream package can pattern-match and rebuild nodes. EP-4
-- does not ship a node-level @rewriteNodes@ helper, so the recursion is spelled out
-- here over every constructor (the EP-4/EP-5 rule: every function over the GADT
-- must match every constructor).
--
-- __Caveat (recorded in the plan's Decision Log).__ The node's existing 'Params'
-- are preserved verbatim. For the common case — compiling a base (uncompiled)
-- program whose nodes carry 'Shikumi.Program.emptyParams' — this reproduces
-- 'Shikumi.Module.chainOfThought' exactly. If a node already carries an
-- @instructionOverride@, that override still replaces the augmented instruction at
-- run time (so the cue is lost): apply chain-of-thought /before/ a zero-shot
-- instruction, or fold the cue into the instruction yourself. If a node already
-- carries demos, they must be chain-of-thought-shaped (include a @reasoning@ field)
-- to decode under the augmented output; otherwise compile chain-of-thought before
-- few-shot.
module Shikumi.Compile.ChainOfThought
  ( chainOfThoughtCompiler,
  )
where

import Shikumi.Compile.Types (Compiler (..))
import Shikumi.Module (chainOfThoughtRaw, value)
import Shikumi.Program
  ( Program
      ( Compose,
        Embed,
        Ensemble,
        FMap,
        MajorityVote,
        Map,
        Parallel,
        Predict,
        Retry,
        RetryWhen,
        Validate
      ),
    mapParams,
  )

-- | Rewrite every 'Predict' node into its chain-of-thought form, recursing through
-- every composite/combinator node so nodes nested arbitrarily deep are reached.
chainOfThoughtCompiler :: Compiler
chainOfThoughtCompiler = Compiler cot

-- | The structural rewrite, polymorphic in @i@/@o@ so it threads through the GADT's
-- existential children. At a 'Predict' leaf the constructor's captured dictionaries
-- ('Shikumi.Schema.FromModel' @i@/@o@, 'Shikumi.Schema.ToSchema' @o@, the
-- 'Shikumi.Adapter.ToPrompt's) are exactly what 'chainOfThoughtRaw' needs.
cot :: Program i o -> Program i o
cot (Predict sig ps) = FMap value (mapParams (const ps) (chainOfThoughtRaw sig))
cot (Compose a b) = Compose (cot a) (cot b)
cot (FMap k p) = FMap k (cot p)
cot (Map w p) = Map w (cot p)
cot (Parallel a b) = Parallel (cot a) (cot b)
cot (Retry n p) = Retry n (cot p)
cot (RetryWhen ok n p) = RetryWhen ok n (cot p)
cot (Validate v p) = Validate v (cot p)
cot (MajorityVote k sched p) = MajorityVote k sched (cot p)
cot (Ensemble ps reduce) = Ensemble (map cot ps) reduce
cot (Embed f) = Embed f -- an agent node has no 'Predict' to rewrite; pass through