shikumi 0.4.1.1 → 0.5.0.0
raw patch · 23 files changed
+1285/−120 lines, 23 filesdep ~baikaidep ~baikai-claudedep ~baikai-effectfulPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: baikai, baikai-claude, baikai-effectful, baikai-openai, shikumi
API changes (from Hackage documentation)
- Shikumi.Adapter: instance (GHC.Internal.Generics.Selector s, Shikumi.Adapter.PromptValue t) => Shikumi.Adapter.GToPromptFields (GHC.Internal.Generics.S1 s (GHC.Internal.Generics.K1 GHC.Internal.Generics.R t))
- Shikumi.Adapter: instance (Shikumi.Adapter.GToPromptFields a, Shikumi.Adapter.GToPromptFields b) => Shikumi.Adapter.GToPromptFields (a GHC.Internal.Generics.:*: b)
- Shikumi.Adapter: instance GHC.Internal.Show.Show a => Shikumi.Adapter.PromptValue a
- Shikumi.Adapter: instance Shikumi.Adapter.GToPromptFields cs => Shikumi.Adapter.GToPromptFields (GHC.Internal.Generics.C1 c cs)
- Shikumi.Adapter: instance Shikumi.Adapter.GToPromptFields cs => Shikumi.Adapter.GToPromptFields (GHC.Internal.Generics.D1 d cs)
- Shikumi.Adapter: instance Shikumi.Adapter.PromptValue Data.Text.Internal.Text
- Shikumi.Adapter: instance Shikumi.Adapter.PromptValue a => Shikumi.Adapter.PromptValue (GHC.Internal.Maybe.Maybe a)
- Shikumi.Adapter: instance Shikumi.Adapter.PromptValue a => Shikumi.Adapter.PromptValue (Shikumi.Schema.Types.Constrained cs a)
- Shikumi.Adapter: instance Shikumi.Adapter.PromptValue a => Shikumi.Adapter.PromptValue [a]
- Shikumi.Module: instance Shikumi.Adapter.ToPrompt Shikumi.Module.ExtractIn
- Shikumi.Module: instance Shikumi.Adapter.ToPrompt o => Shikumi.Adapter.ToPrompt (Shikumi.Module.WithReasoning o)
- Shikumi.Refine: instance (Shikumi.Adapter.ToPrompt i, Shikumi.Adapter.ToPrompt o) => Shikumi.Adapter.ToPrompt (Shikumi.Refine.MultiChainInput i o)
- Shikumi.Refine: instance Shikumi.Adapter.ToPrompt Shikumi.Refine.AdviceIn
- Shikumi.Refine: instance Shikumi.Adapter.ToPrompt Shikumi.Refine.AdviceOut
+ Shikumi.Adapter: ($dmtoEvidenceJSON) :: (ToPrompt a, Generic a, GToPromptFields (Rep a)) => a -> Value
+ Shikumi.Adapter: ($dmtoPromptJSON) :: (ToPrompt a, Generic a, GToPromptFields (Rep a)) => a -> Value
+ Shikumi.Adapter: DecisionOnly :: ModelCapability
+ Shikumi.Adapter: attachDecisionState :: Value -> Options -> Options
+ Shikumi.Adapter: class PromptValue a
+ Shikumi.Adapter: evidenceFields :: ToSchema o => Map Text DecisionSetting -> [Judgment]
+ Shikumi.Adapter: evidenceSchema :: Value -> [Judgment] -> Value
+ Shikumi.Adapter: metaDecisionEvidenceKey :: Text
+ Shikumi.Adapter: metaDecisionSchemaKey :: Text
+ Shikumi.Adapter: metaDecisionStateKey :: Text
+ Shikumi.Adapter: promptEvidenceJSON :: PromptValue a => a -> Value
+ Shikumi.Adapter: promptValue :: PromptValue a => a -> Text
+ Shikumi.Adapter: promptValueJSON :: PromptValue a => a -> Value
+ Shikumi.Adapter: renderEvidenceRequest :: forall i o. (ToSchema o, ToPrompt i, ToPrompt o) => Map Text DecisionSetting -> Signature i o -> i -> (Context, Options)
+ Shikumi.Adapter: responseValue :: Value -> Response -> Either ShikumiError Value
+ Shikumi.Adapter: stripDecisionMarkers :: Value -> Value
+ Shikumi.Adapter: toEvidenceJSON :: ToPrompt a => a -> Value
+ Shikumi.Adapter: toPromptJSON :: ToPrompt a => a -> Value
+ Shikumi.Decision: ($dmchoiceOptions) :: (ChoiceOptions a, Generic a, GChoiceOptions (Rep a)) => [(Text, a, Maybe Text)]
+ Shikumi.Decision: Choice :: !a -> !Map Text Double -> !Maybe Double -> Choice a
+ Shikumi.Decision: ChoiceEvidence :: !Map Text Double -> !Maybe Double -> ChoiceEvidence
+ Shikumi.Decision: FieldChoice :: !ChoiceEvidence -> FieldEvidence
+ Shikumi.Decision: FieldNoul :: !NoulEvidence -> FieldEvidence
+ Shikumi.Decision: FieldScore :: !ScoreEvidence -> FieldEvidence
+ Shikumi.Decision: JudgeBoolean :: !Maybe Value -> !Maybe Value -> JudgmentKind
+ Shikumi.Decision: JudgeChoice :: ![(Text, Maybe Value)] -> JudgmentKind
+ Shikumi.Decision: JudgeOrdered :: ![Value] -> JudgmentKind
+ Shikumi.Decision: Judgment :: !Text -> !Maybe Text -> !JudgmentKind -> Judgment
+ Shikumi.Decision: Noul :: !Bool -> !Double -> !Double -> NoulWith (t :: Symbol) (f :: Symbol)
+ Shikumi.Decision: NoulEvidence :: Double -> NoulEvidence
+ Shikumi.Decision: ScoreEvidence :: !Vector Double -> !Maybe Double -> ScoreEvidence
+ Shikumi.Decision: [Score] :: forall (levels :: [Symbol]). CheckLevels (Length levels) => !Double -> !Int -> !Vector Double -> !Maybe Double -> Score levels
+ Shikumi.Decision: [confidence] :: ScoreEvidence -> !Maybe Double
+ Shikumi.Decision: [judgmentField] :: Judgment -> !Text
+ Shikumi.Decision: [judgmentInstruction] :: Judgment -> !Maybe Text
+ Shikumi.Decision: [judgmentKind] :: Judgment -> !JudgmentKind
+ Shikumi.Decision: [levelProbabilities] :: ScoreEvidence -> !Vector Double
+ Shikumi.Decision: [pTrue] :: NoulEvidence -> Double
+ Shikumi.Decision: [probabilities] :: ChoiceEvidence -> !Map Text Double
+ Shikumi.Decision: [probability] :: NoulWith (t :: Symbol) (f :: Symbol) -> !Double
+ Shikumi.Decision: [value] :: Choice a -> !a
+ Shikumi.Decision: choiceOptions :: ChoiceOptions a => [(Text, a, Maybe Text)]
+ Shikumi.Decision: class Eq a => ChoiceOptions a
+ Shikumi.Decision: class DecisionOutputs o
+ Shikumi.Decision: class IsDecision (a :: k)
+ Shikumi.Decision: data Choice a
+ Shikumi.Decision: data ChoiceEvidence
+ Shikumi.Decision: data FieldEvidence
+ Shikumi.Decision: data Judgment
+ Shikumi.Decision: data JudgmentKind
+ Shikumi.Decision: data NoulWith (t :: Symbol) (f :: Symbol)
+ Shikumi.Decision: data Score (levels :: [Symbol])
+ Shikumi.Decision: data ScoreEvidence
+ Shikumi.Decision: decisionDescription :: IsDecision a => Proxy a -> Maybe Text
+ Shikumi.Decision: decisionKind :: IsDecision a => Proxy a -> JudgmentKind
+ Shikumi.Decision: deriveChoice :: [Text] -> Map Text Double -> ChoiceEvidence -> Either Text Text
+ Shikumi.Decision: deriveNoul :: Double -> NoulEvidence -> (Bool, Double)
+ Shikumi.Decision: deriveScore :: [Double] -> ScoreEvidence -> Either Text (Double, Int)
+ Shikumi.Decision: instance (GHC.Internal.TypeLits.KnownSymbol d, Shikumi.Decision.IsDecision a) => Shikumi.Decision.IsDecision (Shikumi.Schema.Types.Field d a)
+ Shikumi.Decision: instance (GHC.Internal.TypeLits.KnownSymbol name, Shikumi.Decision.RequireDecision name a, Shikumi.Decision.IsDecision a) => Shikumi.Decision.GDecisionOutputs (GHC.Internal.Generics.S1 ('GHC.Internal.Generics.MetaSel ('GHC.Internal.Maybe.Just name) su ss ds) (GHC.Internal.Generics.K1 GHC.Internal.Generics.R a))
+ Shikumi.Decision: instance (GHC.Internal.TypeLits.KnownSymbol s, Shikumi.Decision.KnownLevels ss) => Shikumi.Decision.KnownLevels (s : ss)
+ Shikumi.Decision: instance (GHC.Internal.TypeLits.KnownSymbol t, GHC.Internal.TypeLits.KnownSymbol f) => Shikumi.Decision.IsDecision (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance (GHC.Internal.TypeLits.KnownSymbol t, GHC.Internal.TypeLits.KnownSymbol f) => Shikumi.Schema.ToSchema (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance (Shikumi.Decision.GChoiceOptions a, Shikumi.Decision.GChoiceOptions b) => Shikumi.Decision.GChoiceOptions (a GHC.Internal.Generics.:+: b)
+ Shikumi.Decision: instance (Shikumi.Decision.GDecisionOutputs a, Shikumi.Decision.GDecisionOutputs b) => Shikumi.Decision.GDecisionOutputs (a GHC.Internal.Generics.:*: b)
+ Shikumi.Decision: instance (Shikumi.Decision.KnownLevels ss, Shikumi.Decision.CheckLevels (Shikumi.Decision.Length ss)) => Shikumi.Decision.IsDecision (Shikumi.Decision.Score ss)
+ Shikumi.Decision: instance (Shikumi.Decision.KnownLevels ss, Shikumi.Decision.CheckLevels (Shikumi.Decision.Length ss)) => Shikumi.Schema.FromModel (Shikumi.Decision.Score ss)
+ Shikumi.Decision: instance (Shikumi.Decision.KnownLevels ss, Shikumi.Decision.CheckLevels (Shikumi.Decision.Length ss)) => Shikumi.Schema.ToSchema (Shikumi.Decision.Score ss)
+ Shikumi.Decision: instance Data.Aeson.Types.FromJSON.FromJSON Shikumi.Decision.ChoiceEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.FromJSON.FromJSON Shikumi.Decision.FieldEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.FromJSON.FromJSON Shikumi.Decision.NoulEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.FromJSON.FromJSON Shikumi.Decision.ScoreEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON (Shikumi.Decision.Score ss)
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON Shikumi.Decision.ChoiceEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON Shikumi.Decision.FieldEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON Shikumi.Decision.NoulEvidence
+ Shikumi.Decision: instance Data.Aeson.Types.ToJSON.ToJSON Shikumi.Decision.ScoreEvidence
+ Shikumi.Decision: instance GHC.Classes.Eq (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance GHC.Classes.Eq (Shikumi.Decision.Score levels)
+ Shikumi.Decision: instance GHC.Classes.Eq Shikumi.Decision.ChoiceEvidence
+ Shikumi.Decision: instance GHC.Classes.Eq Shikumi.Decision.FieldEvidence
+ Shikumi.Decision: instance GHC.Classes.Eq Shikumi.Decision.Judgment
+ Shikumi.Decision: instance GHC.Classes.Eq Shikumi.Decision.JudgmentKind
+ Shikumi.Decision: instance GHC.Classes.Eq Shikumi.Decision.NoulEvidence
+ Shikumi.Decision: instance GHC.Classes.Eq Shikumi.Decision.ScoreEvidence
+ Shikumi.Decision: instance GHC.Classes.Eq a => GHC.Classes.Eq (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance GHC.Internal.Generics.Constructor c => Shikumi.Decision.GChoiceOptions (GHC.Internal.Generics.C1 c GHC.Internal.Generics.U1)
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic Shikumi.Decision.ChoiceEvidence
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic Shikumi.Decision.FieldEvidence
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic Shikumi.Decision.Judgment
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic Shikumi.Decision.JudgmentKind
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic Shikumi.Decision.NoulEvidence
+ Shikumi.Decision: instance GHC.Internal.Generics.Generic Shikumi.Decision.ScoreEvidence
+ Shikumi.Decision: instance GHC.Internal.Show.Show (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance GHC.Internal.Show.Show (Shikumi.Decision.Score levels)
+ Shikumi.Decision: instance GHC.Internal.Show.Show Shikumi.Decision.ChoiceEvidence
+ Shikumi.Decision: instance GHC.Internal.Show.Show Shikumi.Decision.FieldEvidence
+ Shikumi.Decision: instance GHC.Internal.Show.Show Shikumi.Decision.Judgment
+ Shikumi.Decision: instance GHC.Internal.Show.Show Shikumi.Decision.JudgmentKind
+ Shikumi.Decision: instance GHC.Internal.Show.Show Shikumi.Decision.NoulEvidence
+ Shikumi.Decision: instance GHC.Internal.Show.Show Shikumi.Decision.ScoreEvidence
+ Shikumi.Decision: instance GHC.Internal.Show.Show a => GHC.Internal.Show.Show (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Shikumi.Adapter.Prompt.PromptValue (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance Shikumi.Adapter.Prompt.PromptValue (Shikumi.Decision.Score ss)
+ Shikumi.Decision: instance Shikumi.Decision.ChoiceOptions a => Shikumi.Adapter.Prompt.PromptValue (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Shikumi.Decision.ChoiceOptions a => Shikumi.Decision.IsDecision (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Shikumi.Decision.ChoiceOptions a => Shikumi.Schema.FromModel (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Shikumi.Decision.ChoiceOptions a => Shikumi.Schema.ToSchema (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Shikumi.Decision.GChoiceOptions f => Shikumi.Decision.GChoiceOptions (GHC.Internal.Generics.D1 d f)
+ Shikumi.Decision: instance Shikumi.Decision.GDecisionOutputs f => Shikumi.Decision.GDecisionOutputs (GHC.Internal.Generics.C1 c f)
+ Shikumi.Decision: instance Shikumi.Decision.GDecisionOutputs f => Shikumi.Decision.GDecisionOutputs (GHC.Internal.Generics.D1 d f)
+ Shikumi.Decision: instance Shikumi.Decision.IsDecision GHC.Types.Bool
+ Shikumi.Decision: instance Shikumi.Decision.KnownLevels '[]
+ Shikumi.Decision: instance Shikumi.Schema.FromModel (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance Shikumi.Schema.Validatable (Shikumi.Decision.Choice a)
+ Shikumi.Decision: instance Shikumi.Schema.Validatable (Shikumi.Decision.NoulWith t f)
+ Shikumi.Decision: instance Shikumi.Schema.Validatable (Shikumi.Decision.Score ss)
+ Shikumi.Decision: judgmentsInSchema :: Value -> ([Judgment], [Text])
+ Shikumi.Decision: judgmentsOf :: DecisionOutputs o => [Judgment]
+ Shikumi.Decision: newtype NoulEvidence
+ Shikumi.Decision: questionJSON :: Judgment -> Value
+ Shikumi.Decision: renderDecisionState :: (ToPrompt i, ToPrompt o) => Signature i o -> i -> Value
+ Shikumi.Decision: type Noul = NoulWith "" ""
+ Shikumi.Decision.Runtime: decodeDecisionEvidence :: Judgment -> Value -> Either ShikumiError FieldEvidence
+ Shikumi.Decision.Runtime: evidenceObject :: Value -> [Judgment] -> Map Text DecisionSetting -> Map Text FieldEvidence -> Either ShikumiError Value
+ Shikumi.Decision.Runtime: evidenceOf :: [Judgment] -> Response -> Maybe (Either ShikumiError (Map Text FieldEvidence))
+ Shikumi.Decision.Setting: ChoiceSetting :: !Map Text Double -> DecisionSetting
+ Shikumi.Decision.Setting: NoulSetting :: !Double -> DecisionSetting
+ Shikumi.Decision.Setting: ScoreSetting :: ![Double] -> DecisionSetting
+ Shikumi.Decision.Setting: [cuts] :: DecisionSetting -> ![Double]
+ Shikumi.Decision.Setting: [threshold] :: DecisionSetting -> !Double
+ Shikumi.Decision.Setting: [weights] :: DecisionSetting -> !Map Text Double
+ Shikumi.Decision.Setting: data DecisionSetting
+ Shikumi.Decision.Setting: defaultSetting :: Judgment -> DecisionSetting
+ Shikumi.Decision.Setting: deriveField :: Judgment -> Maybe DecisionSetting -> FieldEvidence -> Either ShikumiError Value
+ Shikumi.Decision.Setting: instance Data.Aeson.Types.FromJSON.FromJSON Shikumi.Decision.Setting.DecisionSetting
+ Shikumi.Decision.Setting: instance Data.Aeson.Types.ToJSON.ToJSON Shikumi.Decision.Setting.DecisionSetting
+ Shikumi.Decision.Setting: instance GHC.Classes.Eq Shikumi.Decision.Setting.DecisionSetting
+ Shikumi.Decision.Setting: instance GHC.Internal.Generics.Generic Shikumi.Decision.Setting.DecisionSetting
+ Shikumi.Decision.Setting: instance GHC.Internal.Show.Show Shikumi.Decision.Setting.DecisionSetting
+ Shikumi.Decision.Setting: validateSetting :: Judgment -> DecisionSetting -> Either Text DecisionSetting
+ Shikumi.Module: instance Shikumi.Adapter.Prompt.ToPrompt Shikumi.Module.ExtractIn
+ Shikumi.Module: instance Shikumi.Adapter.Prompt.ToPrompt o => Shikumi.Adapter.Prompt.ToPrompt (Shikumi.Module.WithReasoning o)
+ Shikumi.Program: [decisionSettings] :: Params -> !Map Text DecisionSetting
+ Shikumi.Program: clearDecisionSetting :: Text -> Params -> Params
+ Shikumi.Program: decisionSettingFor :: Text -> Params -> Maybe DecisionSetting
+ Shikumi.Program: nodeDecisionFieldsIndexed :: Program i o -> [[Judgment]]
+ Shikumi.Program: parseResponseWithSettings :: forall i o. (FromModel o, ToSchema o, Validatable o, ToPrompt i, ToPrompt o) => Map Text DecisionSetting -> Signature i o -> Response -> Either ShikumiError o
+ Shikumi.Program: setDecisionSetting :: Text -> DecisionSetting -> Params -> Params
+ Shikumi.Refine: instance (Shikumi.Adapter.Prompt.ToPrompt i, Shikumi.Adapter.Prompt.ToPrompt o) => Shikumi.Adapter.Prompt.ToPrompt (Shikumi.Refine.MultiChainInput i o)
+ Shikumi.Refine: instance Shikumi.Adapter.Prompt.ToPrompt Shikumi.Refine.AdviceIn
+ Shikumi.Refine: instance Shikumi.Adapter.Prompt.ToPrompt Shikumi.Refine.AdviceOut
- Shikumi.Program: Params :: !Maybe Text -> ![Demo] -> Params
+ Shikumi.Program: Params :: !Maybe Text -> ![Demo] -> !Map Text DecisionSetting -> Params
Files
- CHANGELOG.md +27/−0
- shikumi.cabal +19/−11
- src/Shikumi/Adapter.hs +98/−70
- src/Shikumi/Adapter/Prompt.hs +139/−0
- src/Shikumi/Decision.hs +507/−0
- src/Shikumi/Decision/Runtime.hs +98/−0
- src/Shikumi/Decision/Setting.hs +85/−0
- src/Shikumi/LLM/Defaults.hs +6/−2
- src/Shikumi/Module.hs +5/−1
- src/Shikumi/Program.hs +110/−15
- src/Shikumi/Refine.hs +3/−1
- src/Shikumi/Routing.hs +26/−10
- src/Shikumi/Stream.hs +6/−4
- test/DecisionDeriveSpec.hs +94/−0
- test/ErrorSpec.hs +2/−1
- test/Main.hs +2/−0
- test/ProgramAcceptanceSpec.hs +2/−1
- test/RequestDefaultsSpec.hs +23/−1
- test/RoutingSpec.hs +2/−1
- test/SerializeSpec.hs +2/−2
- test/compile-fail/FreeFormDecision.hs +13/−0
- test/compile-fail/TooFewScoreLevels.hs +8/−0
- test/compile-fail/TooManyScoreLevels.hs +8/−0
CHANGELOG.md view
@@ -2,6 +2,33 @@ ## Unreleased +## 0.5.0.0 — 2026-11-02++### Added++- Add probability-backed `NoulWith`/`Noul`, `Choice` and `Score` outputs on+ judgment-only and generative models, with closed evidence schemas, strict+ decoding and shared local answer derivation.+- Add persistent per-field thresholds, score cuts and choice weights. Setting+ changes reuse cached or replayed evidence without changing request identity.+- Add structured prompt and evidence rendering through `ToPrompt.toPromptJSON`,+ `toEvidenceJSON` and `PromptValue` methods.++### Changed++- **Breaking:** `Params` gains a decision-settings field; use `emptyParams`+ record updates instead of positional construction. Custom `ToPrompt` instances+ without generic defaults must implement the new structured rendering methods.+- **Breaking:** `ModelCapability` gains `DecisionOnly`; update exhaustive matches.+- Route judgment-only models through typed structured state. Only evidence defaults+ apply to these models; explicit generation controls retain typed refusals.+- Refuse decision streaming before dispatch. Nested decision containers, recursive+ sessions and ReAct decision submissions remain unsupported.+- Require `mori://shinzui/baikai/packages/baikai` `>=0.10.0.0 && <0.11`,+ `mori://shinzui/baikai/packages/baikai-claude` and+ `mori://shinzui/baikai/packages/baikai-openai` `>=0.7.3.0 && <0.8`, and+ `mori://shinzui/baikai/packages/baikai-effectful` `>=0.4.0.5 && <0.5`.+ ## 0.4.1.1 — 2026-10-16 ### Changed
shikumi.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: shikumi-version: 0.4.1.1+version: 0.5.0.0 synopsis: Typed, structured, evaluable LM programs over baikai category: AI description:@@ -13,6 +13,7 @@ maintainer: nadeem@gmail.com build-type: Simple extra-doc-files: CHANGELOG.md+extra-source-files: test/compile-fail/*.hs common common-options ghc-options:@@ -41,6 +42,9 @@ Shikumi.Adapter Shikumi.Combinator Shikumi.Compaction+ Shikumi.Decision+ Shikumi.Decision.Runtime+ Shikumi.Decision.Setting Shikumi.Effect.Time Shikumi.Error Shikumi.LLM@@ -60,13 +64,16 @@ Shikumi.Signature Shikumi.Stream - other-modules: Shikumi.Adapter.Xml+ other-modules:+ Shikumi.Adapter.Prompt+ Shikumi.Adapter.Xml+ build-depends: aeson >=2.2 && <2.3,- baikai >=0.8.0.0 && <0.9,- baikai-claude >=0.7.1.1 && <0.8,- baikai-effectful >=0.4.0.3 && <0.5,- baikai-openai >=0.7.1.1 && <0.8,+ baikai >=0.10.0.0 && <0.11,+ baikai-claude >=0.7.3.0 && <0.8,+ baikai-effectful >=0.4.0.5 && <0.5,+ baikai-openai >=0.7.3.0 && <0.8, base >=4.20 && <5, base64-bytestring >=1.2 && <1.3, bytestring >=0.11 && <0.13,@@ -96,6 +103,7 @@ CombinatorSpec ConstraintSpec ContinuationSpec+ DecisionDeriveSpec EndToEndSpec ErrorSpec FallbackGuideSpec@@ -128,10 +136,10 @@ build-depends: QuickCheck, aeson,- baikai >=0.8.0.0 && <0.9,- baikai-claude >=0.7.1.1 && <0.8,- baikai-effectful >=0.4.0.3 && <0.5,- baikai-openai >=0.7.1.1 && <0.8,+ baikai >=0.10.0.0 && <0.11,+ baikai-claude >=0.7.3.0 && <0.8,+ baikai-effectful >=0.4.0.5 && <0.5,+ baikai-openai >=0.7.3.0 && <0.8, base, base64-bytestring, bytestring,@@ -140,7 +148,7 @@ effectful, generic-lens, lens,- shikumi ^>=0.4.1.1,+ shikumi ^>=0.5.0.0, stm, streamly-core, tasty,
src/Shikumi/Adapter.hs view
@@ -38,6 +38,7 @@ module Shikumi.Adapter ( -- * Input rendering ToPrompt (..),+ PromptValue (..), -- * The seam Adapter (..),@@ -58,6 +59,15 @@ metaTemperatureKey, metaNativePromptKey, metaNativeDemosKey,+ metaDecisionStateKey,+ metaDecisionSchemaKey,+ metaDecisionEvidenceKey,+ evidenceFields,+ evidenceSchema,+ renderEvidenceRequest,+ responseValue,+ attachDecisionState,+ stripDecisionMarkers, attachNativeRender, nativeRenderPieces, renderOutputNative,@@ -82,6 +92,7 @@ user, userImage, )+import Baikai.Model (isJudgmentOnly) import Control.Lens (at, (&), (.~), (?~), (^.)) import Data.Aeson (Object, Value (..), eitherDecodeStrict, toJSON) import Data.Aeson qualified as Aeson@@ -89,7 +100,6 @@ import Data.Aeson.KeyMap qualified as KM import Data.ByteString.Lazy qualified as LBS import Data.Generics.Labels ()-import Data.Kind (Type) import Data.List (sortOn) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map@@ -98,13 +108,15 @@ import Data.Text qualified as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Vector qualified as V-import GHC.Generics+import Shikumi.Adapter.Prompt import Shikumi.Adapter.Xml (decodeXmlFields, renderXmlFields, xmlSchemaGuide)+import Shikumi.Decision (Judgment (..), JudgmentKind (..), judgmentsInSchema, questionJSON)+import Shikumi.Decision.Setting (DecisionSetting) import Shikumi.Error (ShikumiError (..))-import Shikumi.Multimodal (GImageFieldNames (..), GImageFields (..), Image, imageToContent)+import Shikumi.Multimodal (imageToContent) import Shikumi.Schema (FromModel, ToSchema, Validatable, deriveSchema, fromModelChecked)-import Shikumi.Schema.Types (Constrained (..), FieldMeta (..))-import Shikumi.Signature (Demo (..), Signature, getDemos, getInstruction, outputFields)+import Shikumi.Schema.Types (FieldMeta (..))+import Shikumi.Signature (Demo (..), Signature, getDemos, getInstruction, inputFields, outputFields) -- --------------------------------------------------------------------------- -- ToPrompt: render an input record (and demo outputs) as labeled text@@ -112,70 +124,6 @@ -- | Render a record as labeled text for the prompt. 'toPromptFields' yields the -- @(fieldName, valueText)@ pairs; 'toPrompt' joins them as @"name: value"@ lines.-class ToPrompt a where- toPromptFields :: a -> [(Text, Text)]- default toPromptFields :: (Generic a, GToPromptFields (Rep a)) => a -> [(Text, Text)]- toPromptFields = gToPromptFields . from-- toPrompt :: a -> Text- toPrompt = T.intercalate "\n" . map (\(k, v) -> k <> ": " <> v) . toPromptFields-- -- | The 'Image' values an /input/ record carries, in field order (EP-24). The- -- adapter lowers these to baikai @UserImage@ blocks in 'userTurn'. The generic- -- default walks the record, so any 'Generic' input gets it for free; a record- -- with no image field yields @[]@ and renders byte-for-byte as before. A- -- hand-written instance whose fields are polymorphic (e.g.- -- "Shikumi.Module".@WithReasoning@) cannot be walked generically and overrides- -- this to @[]@.- imageFields :: a -> [Image]- default imageFields :: (Generic a, GImageFields (Rep a)) => a -> [Image]- imageFields = gImageFields . from-- -- | The names of an input record's image fields, in field order (EP-24). The- -- text renderer in 'userTurn' drops these so an image is not also rendered as- -- prose. Same defaulting story as 'imageFields'.- imageFieldNames :: a -> [Text]- default imageFieldNames :: (Generic a, GImageFieldNames (Rep a)) => a -> [Text]- imageFieldNames = gImageFieldNames . from--class GToPromptFields (f :: Type -> Type) where- gToPromptFields :: f p -> [(Text, Text)]--instance (GToPromptFields cs) => GToPromptFields (D1 d cs) where- gToPromptFields (M1 x) = gToPromptFields x--instance (GToPromptFields cs) => GToPromptFields (C1 c cs) where- gToPromptFields (M1 x) = gToPromptFields x--instance (GToPromptFields a, GToPromptFields b) => GToPromptFields (a :*: b) where- gToPromptFields (a :*: b) = gToPromptFields a ++ gToPromptFields b--instance (Selector s, PromptValue t) => GToPromptFields (S1 s (K1 R t)) where- gToPromptFields m@(M1 (K1 v)) = [(T.pack (selName m), promptValue v)]---- | Render a leaf value to text. Text stays as-is; lists join with commas;--- 'Maybe' renders its contents or @""@; a 'Shikumi.Schema.Types.Field' unwraps;--- everything else falls back to 'Show'.-class PromptValue a where- promptValue :: a -> Text--instance {-# OVERLAPPABLE #-} (Show a) => PromptValue a where- promptValue = T.pack . show--instance PromptValue Text where- promptValue = id--instance (PromptValue a) => PromptValue [a] where- promptValue = T.intercalate ", " . map promptValue--instance (PromptValue a) => PromptValue (Maybe a) where- promptValue = maybe "" promptValue---- | A t'Constrained' field renders as its inner value (the constraint is a--- compile-time/decode-time concern, not a prompt concern). EP-26.-instance (PromptValue a) => PromptValue (Constrained cs a) where- promptValue = promptValue . unConstrained- -- --------------------------------------------------------------------------- -- The seam -- ---------------------------------------------------------------------------@@ -187,7 +135,7 @@ } -- | Whether a model supports provider-native structured output.-data ModelCapability = NativeSchema | PromptFallback+data ModelCapability = NativeSchema | PromptFallback | DecisionOnly deriving stock (Eq, Show) -- | A pure capability check over a baikai t'Model', derived from baikai's@@ -213,6 +161,7 @@ -- separate capability (baikai's CLI providers enforce schemas but drop tools); -- see "Shikumi.Agent.ReAct". capabilityFor :: Model -> ModelCapability+capabilityFor m | isJudgmentOnly m = DecisionOnly capabilityFor m = case declaredStructuredOutput api of NoStructuredOutput -> PromptFallback NativeJsonSchema@@ -235,6 +184,7 @@ Model -> Adapter i o adapterFor m = case capabilityFor m of+ DecisionOnly -> nativeAdapter NativeSchema -> nativeAdapter PromptFallback -> fallbackAdapter @@ -267,6 +217,20 @@ metaNativeDemosKey :: Text metaNativeDemosKey = "shikumi.native.demos" +-- | Reserved structured state for judgment routing. Stripped before transport.+metaDecisionStateKey :: Text+metaDecisionStateKey = "shikumi.decision.state"++-- | Attach decision state without changing the native or marker prompt.+attachDecisionState :: Value -> Options -> Options+attachDecisionState state opts = opts & #metadata . at metaDecisionStateKey ?~ state++-- | Remove private decoder metadata recursively before sending a schema.+stripDecisionMarkers :: Value -> Value+stripDecisionMarkers (Object o) = Object (fmap stripDecisionMarkers (KM.delete "x-shikumi-decision" o))+stripDecisionMarkers (Array xs) = Array (fmap stripDecisionMarkers xs)+stripDecisionMarkers v = v+ -- | Stamp the derived JSON schema onto a request's private metadata channel. This -- is no longer a no-op: it records the schema under 'metaResponseSchemaKey' so the -- router can attach a real @responseFormat@ once the ambient model is known. (The@@ -650,3 +614,67 @@ Just (Array alts) -> any isStringLike (V.toList alts) _ -> False isStringLike _ = False++-- | Evidence mode and its original answer schema are independent of numeric+-- settings. The router chooses the answer schema for judgment-only models.+metaDecisionEvidenceKey, metaDecisionSchemaKey :: Text+metaDecisionEvidenceKey = "shikumi.decision.evidence"+metaDecisionSchemaKey = "shikumi.decision.schema"++evidenceFields :: forall o. (ToSchema o) => Map Text DecisionSetting -> [Judgment]+evidenceFields settings = filter selected (fst (judgmentsInSchema schema))+ where+ schema = deriveSchema @o+ selected j =+ Map.member (judgmentField j) settings || case propertySchema (judgmentField j) schema of+ Just (Object p) -> KM.member "x-shikumi-decision" p+ _ -> False++-- | Replace selected answer properties with closed probability objects.+evidenceSchema :: Value -> [Judgment] -> Value+evidenceSchema (Object root) fields = case KM.lookup "properties" root of+ Just (Object props) -> Object (KM.insert "properties" (Object (foldl replace props fields)) root)+ _ -> Object root+ where+ probability = Aeson.object ["type" Aeson..= ("number" :: Text), "minimum" Aeson..= (0 :: Int), "maximum" Aeson..= (1 :: Int)]+ closed props = Aeson.object ["type" Aeson..= ("object" :: Text), "properties" Aeson..= Object props, "required" Aeson..= map Key.toText (KM.keys props), "additionalProperties" Aeson..= False]+ replace props j =+ let keys = case judgmentKind j of JudgeBoolean {} -> []; JudgeChoice ls -> map fst ls; JudgeOrdered ls -> map (T.pack . show) [0 .. length ls - 1]+ shape = case judgmentKind j of+ JudgeBoolean {} -> closed (KM.singleton "noul" probability)+ _ -> closed (KM.fromList [("probabilities", closed (KM.fromList [(Key.fromText k, probability) | k <- keys])), ("confidence", probability)])+ described = case shape of Object p -> Object (KM.insert "description" (String (jsonText (questionJSON j))) p); _ -> shape+ in KM.insert (Key.fromText (judgmentField j)) described props+evidenceSchema v _ = v++jsonText :: Value -> Text+jsonText = decodeUtf8 . LBS.toStrict . Aeson.encode++-- | Render probability requests without demonstration turns. Task examples keep+-- labels or actual evidence and never invent probability distributions.+renderEvidenceRequest :: forall i o. (ToSchema o, ToPrompt i, ToPrompt o) => Map Text DecisionSetting -> Signature i o -> i -> (Context, Options)+renderEvidenceRequest settings sig input =+ let fields = evidenceFields @o settings+ original = deriveSchema @o+ schema = evidenceSchema original fields+ example (Demo i o) = case (toPromptJSON i, toEvidenceJSON o) of+ (Object a, Object b) -> Object (KM.fromList [(k, v) | (k, v) <- KM.toList (KM.union b a), Key.toText k `elem` map fieldName (inputFields sig ++ outputFields sig)])+ _ -> Null+ examples = if null (getDemos sig) then "" else "\n\nTask examples (labels or evidence):\n" <> jsonText (toJSON (map example (getDemos sig)))+ header = getInstruction sig <> examples <> "\n\nEach probability and confidence must be between 0 and 1. Choice and Score probabilities should sum to one.\n"+ describe f = case filter ((== fieldName f) . judgmentField) fields of+ j : _ -> jsonText (questionJSON j)+ [] -> fromMaybe "" (fieldDesc f)+ native = header <> "Reply with a JSON object containing these fields:\n" <> T.unlines [fieldName f <> ": " <> describe f | f <- outputFields sig]+ markerGuide = header <> "Reply using these sections, each marker on its own line:\n" <> T.unlines [marker (fieldName f) <> " -- " <> describe f <> maybe "" (\s -> "\nJSON shape: " <> renderShape s) (propertySchema (fieldName f) schema) | f <- outputFields sig] <> marker "completed"+ opts =+ attachNativeRender native [] (attachSchema schema emptyOptions)+ & #metadata . at metaDecisionEvidenceKey ?~ toJSON [Aeson.object ["field" Aeson..= judgmentField j, "kind" Aeson..= (case judgmentKind j of JudgeBoolean {} -> "noul"; JudgeChoice {} -> "choice"; JudgeOrdered {} -> "score" :: Text)] | j <- fields]+ & #metadata . at metaDecisionSchemaKey ?~ original+ in (buildContext markerGuide [userTurn input], opts)++-- | Read either JSON text or marker sections with schema-driven object coercion.+responseValue :: Value -> Response -> Either ShikumiError Value+responseValue schema response = case assistantJSON response of+ Right value -> Right value+ Left _ -> Right (sectionsToObject schema (parseMarkers (responseText response)))
+ src/Shikumi/Adapter/Prompt.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Prompt records and typed leaves, shared by ordinary and decision rendering.+module Shikumi.Adapter.Prompt (ToPrompt (..), PromptValue (..)) where++import Data.Aeson (Value (..), toJSON)+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KM+import Data.Kind (Type)+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics+import Shikumi.Multimodal (GImageFieldNames (..), GImageFields (..), Image)+import Shikumi.Schema.Types (Constrained (..), Field (..))++class ToPrompt a where+ toPromptFields :: a -> [(Text, Text)]+ default toPromptFields :: (Generic a, GToPromptFields (Rep a)) => a -> [(Text, Text)]+ toPromptFields = gToPromptFields . from++ toPrompt :: a -> Text+ toPrompt = T.intercalate "\n" . map (\(k, v) -> k <> ": " <> v) . toPromptFields++ -- | Structured fields for decision state. Unlike prompt text, strings such as+ -- @"null"@ or @"123"@ stay strings, and numbers, lists and optional values keep+ -- their JSON types. Override for a custom input representation.+ toPromptJSON :: a -> Value+ default toPromptJSON :: (Generic a, GToPromptFields (Rep a)) => a -> Value+ toPromptJSON = Object . KM.fromList . gToPromptJSON . from++ -- | Demonstration JSON retaining rich decision evidence when available.+ toEvidenceJSON :: a -> Value+ default toEvidenceJSON :: (Generic a, GToPromptFields (Rep a)) => a -> Value+ toEvidenceJSON = Object . KM.fromList . gToEvidenceJSON . from++ -- | The 'Image' values an /input/ record carries, in field order (EP-24). The+ -- adapter lowers these to baikai @UserImage@ blocks in @userTurn@. The generic+ -- default walks the record, so any 'Generic' input gets it for free; a record+ -- with no image field yields @[]@ and renders byte-for-byte as before. A+ -- hand-written instance whose fields are polymorphic (e.g.+ -- "Shikumi.Module".@WithReasoning@) cannot be walked generically and overrides+ -- this to @[]@.+ imageFields :: a -> [Image]+ default imageFields :: (Generic a, GImageFields (Rep a)) => a -> [Image]+ imageFields = gImageFields . from++ -- | The names of an input record's image fields, in field order (EP-24). The+ -- text renderer in @userTurn@ drops these so an image is not also rendered as+ -- prose. Same defaulting story as 'imageFields'.+ imageFieldNames :: a -> [Text]+ default imageFieldNames :: (Generic a, GImageFieldNames (Rep a)) => a -> [Text]+ imageFieldNames = gImageFieldNames . from++class GToPromptFields (f :: Type -> Type) where+ gToPromptFields :: f p -> [(Text, Text)]+ gToPromptJSON :: f p -> [(Key.Key, Value)]+ gToEvidenceJSON :: f p -> [(Key.Key, Value)]++instance (GToPromptFields cs) => GToPromptFields (D1 d cs) where+ gToPromptFields (M1 x) = gToPromptFields x+ gToPromptJSON (M1 x) = gToPromptJSON x+ gToEvidenceJSON (M1 x) = gToEvidenceJSON x++instance (GToPromptFields cs) => GToPromptFields (C1 c cs) where+ gToPromptFields (M1 x) = gToPromptFields x+ gToPromptJSON (M1 x) = gToPromptJSON x+ gToEvidenceJSON (M1 x) = gToEvidenceJSON x++instance (GToPromptFields a, GToPromptFields b) => GToPromptFields (a :*: b) where+ gToPromptFields (a :*: b) = gToPromptFields a ++ gToPromptFields b+ gToPromptJSON (a :*: b) = gToPromptJSON a ++ gToPromptJSON b+ gToEvidenceJSON (a :*: b) = gToEvidenceJSON a ++ gToEvidenceJSON b++instance (Selector s, PromptValue t) => GToPromptFields (S1 s (K1 R t)) where+ gToPromptFields m@(M1 (K1 v)) = [(T.pack (selName m), promptValue v)]+ gToPromptJSON m@(M1 (K1 v)) = [(Key.fromString (selName m), promptValueJSON v)]+ gToEvidenceJSON m@(M1 (K1 v)) = [(Key.fromString (selName m), promptEvidenceJSON v)]++-- | Render a leaf value to text. Text stays as-is; lists join with commas;+-- 'Maybe' renders its contents or @""@; a 'Shikumi.Schema.Types.Field' unwraps;+-- everything else falls back to 'Show'.+class PromptValue a where+ promptValue :: a -> Text++ -- | JSON counterpart of the prompt leaf. Custom structured leaves should+ -- override this instead of encoding JSON through 'Show'.+ promptValueJSON :: a -> Value+ promptValueJSON = String . promptValue++ -- | Full demonstration evidence; ordinary leaves keep their structured value.+ promptEvidenceJSON :: a -> Value+ promptEvidenceJSON = promptValueJSON++instance {-# OVERLAPPABLE #-} (Show a) => PromptValue a where+ promptValue = T.pack . show++instance PromptValue Text where+ promptValue = id++instance PromptValue Bool where+ promptValue = T.pack . show+ promptValueJSON = toJSON++instance PromptValue Int where+ promptValue = T.pack . show+ promptValueJSON = toJSON++instance PromptValue Integer where+ promptValue = T.pack . show+ promptValueJSON = toJSON++instance PromptValue Double where+ promptValue = T.pack . show+ promptValueJSON = toJSON++instance (PromptValue a) => PromptValue [a] where+ promptValue = T.intercalate ", " . map promptValue+ promptValueJSON = toJSON . map promptValueJSON++instance (PromptValue a) => PromptValue (Maybe a) where+ promptValue = maybe "" promptValue+ promptValueJSON = maybe Null promptValueJSON++-- Preserve the existing textual representation of described fields. Decision+-- state uses the unwrapped JSON value without changing legacy prompts.+instance (Show a, PromptValue a) => PromptValue (Field d a) where+ promptValue = T.pack . show+ promptValueJSON = promptValueJSON . unField+ promptEvidenceJSON = promptEvidenceJSON . unField++-- | A t'Constrained' field renders as its inner value (the constraint is a+-- compile-time/decode-time concern, not a prompt concern). EP-26.+instance (PromptValue a) => PromptValue (Constrained cs a) where+ promptValue = promptValue . unConstrained+ promptValueJSON = promptValueJSON . unConstrained+ promptEvidenceJSON = promptEvidenceJSON . unConstrained
+ src/Shikumi/Decision.hs view
@@ -0,0 +1,507 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Closed decisions and their probability evidence. Answers are derived+-- locally; schemas describe judgments, while JSON decoding consumes evidence.+-- No provider transport or provider-specific response type lives here.+module Shikumi.Decision+ ( NoulWith (..),+ Noul,+ Choice (..),+ Score (..),+ NoulEvidence (..),+ ChoiceEvidence (..),+ ScoreEvidence (..),+ FieldEvidence (..),+ ChoiceOptions (..),+ IsDecision (..),+ DecisionOutputs,+ judgmentsOf,+ Judgment (..),+ JudgmentKind (..),+ judgmentsInSchema,+ questionJSON,+ renderDecisionState,+ deriveNoul,+ deriveScore,+ deriveChoice,+ )+where++import Control.Applicative ((<|>))+import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withObject, (.:), (.:?), (.=))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KM+import Data.Aeson.Types (Pair, Parser, parseEither)+import Data.Bifunctor (first)+import Data.Kind (Constraint, Type)+import Data.List (sort)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (fromMaybe)+import Data.Proxy (Proxy (..))+import Data.Text (Text)+import Data.Text qualified as T+import Data.Vector (Vector)+import Data.Vector qualified as V+import GHC.Generics+import GHC.TypeLits (ErrorMessage (..), KnownSymbol, Nat, Symbol, TypeError, symbolVal, type (+))+import Shikumi.Adapter.Prompt (PromptValue (..), ToPrompt (..))+import Shikumi.Error (ShikumiError (..))+import Shikumi.Schema (FromModel (..), ToSchema (..), Validatable)+import Shikumi.Schema.Types (Field, FieldMeta (..), FieldPath, renderPath)+import Shikumi.Signature (Demo (..), Signature, getDemos, getInstruction, inputFields)++-- Evidence JSON is shared by provider-backed and generative decisions.+newtype NoulEvidence = NoulEvidence {pTrue :: Double}+ deriving stock (Eq, Show, Generic)++data ChoiceEvidence = ChoiceEvidence+ {probabilities :: !(Map Text Double), confidence :: !(Maybe Double)}+ deriving stock (Eq, Show, Generic)++data ScoreEvidence = ScoreEvidence+ {levelProbabilities :: !(Vector Double), confidence :: !(Maybe Double)}+ deriving stock (Eq, Show, Generic)++data FieldEvidence = FieldNoul !NoulEvidence | FieldChoice !ChoiceEvidence | FieldScore !ScoreEvidence+ deriving stock (Eq, Show, Generic)++instance ToJSON NoulEvidence where+ toJSON (NoulEvidence p) = object ["noul" .= p]++instance FromJSON NoulEvidence where+ parseJSON = withObject "NoulEvidence" $ \o -> NoulEvidence <$> (o .: "noul" >>= probabilityP)++instance ToJSON ChoiceEvidence where+ toJSON (ChoiceEvidence ps c) = object ["probabilities" .= ps, "confidence" .= c]++instance FromJSON ChoiceEvidence where+ parseJSON = withObject "ChoiceEvidence" $ \o -> do+ ps <- o .: "probabilities" >>= traverse probabilityP+ if Map.null ps then fail "empty probability distribution" else pure ()+ c <- o .:? "confidence" >>= traverse probabilityP+ pure (ChoiceEvidence ps c)++instance ToJSON ScoreEvidence where+ toJSON (ScoreEvidence ps c) = object ["probabilities" .= indexedProbabilities ps, "confidence" .= c]++instance FromJSON ScoreEvidence where+ parseJSON = withObject "ScoreEvidence" $ \o -> do+ ps <- o .: "probabilities" >>= traverse probabilityP+ let n = Map.size ps+ keys = map (T.pack . show) [0 .. n - 1]+ if n < 2 || n > 10 || sort keys /= Map.keys ps+ then fail "score probabilities must have contiguous indices 0..N-1, with 2 to 10 levels"+ else pure ()+ c <- o .:? "confidence" >>= traverse probabilityP+ pure (ScoreEvidence (V.fromList [ps Map.! k | k <- keys]) c)++-- A wrapper needs a tag because numeric Choice labels and Score indices overlap.+instance ToJSON FieldEvidence where+ toJSON e = case e of+ FieldNoul x -> tagged "noul" (toJSON x)+ FieldChoice x -> tagged "choice" (toJSON x)+ FieldScore x -> tagged "score" (toJSON x)+ where+ tagged :: Text -> Value -> Value+ tagged k v = object ["kind" .= k, "evidence" .= v]++instance FromJSON FieldEvidence where+ parseJSON = withObject "FieldEvidence" $ \o -> do+ kind <- o .: "kind" :: Parser Text+ case kind of+ "noul" -> FieldNoul <$> o .: "evidence"+ "choice" -> FieldChoice <$> o .: "evidence"+ "score" -> FieldScore <$> o .: "evidence"+ _ -> fail "unknown decision evidence kind"++probabilityP :: Double -> Parser Double+probabilityP p+ | finite p && p >= 0 && p <= 1 = pure p+ | otherwise = fail "probability or confidence must be finite and in [0,1]"++finite :: Double -> Bool+finite p = not (isNaN p || isInfinite p)++indexedProbabilities :: Vector Double -> Map Text Double+indexedProbabilities ps = Map.fromList [(T.pack (show i), p) | (i, p) <- zip [0 :: Int ..] (V.toList ps)]++data NoulWith (t :: Symbol) (f :: Symbol) = Noul+ {value :: !Bool, probability :: !Double, confidence :: !Double}+ deriving stock (Eq, Show, Generic)++type Noul = NoulWith "" ""++data Choice a = Choice+ {value :: !a, probabilities :: !(Map Text Double), confidence :: !(Maybe Double)}+ deriving stock (Eq, Show, Generic)++-- The constructor carries the level-count constraint as well as the schema+-- instance, so an invalid Score cannot be constructed outside schema derivation.+data Score (levels :: [Symbol]) where+ Score ::+ (CheckLevels (Length levels)) =>+ {value :: !Double, level :: !Int, levelProbabilities :: !(Vector Double), confidence :: !(Maybe Double)} ->+ Score levels++deriving stock instance Eq (Score levels)++deriving stock instance Show (Score levels)++-- | Labels in declaration order, with optional criteria. Override the generic+-- nullary-enum default to use domain labels and descriptions.+class (Eq a) => ChoiceOptions a where+ choiceOptions :: [(Text, a, Maybe Text)]+ default choiceOptions :: (Generic a, GChoiceOptions (Rep a)) => [(Text, a, Maybe Text)]+ choiceOptions = [(n, to v, Nothing) | (n, v) <- gChoiceOptions]++class GChoiceOptions (f :: Type -> Type) where+ gChoiceOptions :: [(Text, f p)]++instance (GChoiceOptions f) => GChoiceOptions (D1 d f) where+ gChoiceOptions = [(n, M1 v) | (n, v) <- gChoiceOptions @f]++instance (GChoiceOptions a, GChoiceOptions b) => GChoiceOptions (a :+: b) where+ gChoiceOptions = [(n, L1 v) | (n, v) <- gChoiceOptions @a] ++ [(n, R1 v) | (n, v) <- gChoiceOptions @b]++instance (Constructor c) => GChoiceOptions (C1 c U1) where+ gChoiceOptions = [(T.pack (conName (undefined :: C1 c U1 ())), M1 U1)]++class KnownLevels (ss :: [Symbol]) where+ levelNames :: Proxy ss -> [Text]++instance KnownLevels '[] where levelNames _ = []++instance (KnownSymbol s, KnownLevels ss) => KnownLevels (s ': ss) where+ levelNames _ = T.pack (symbolVal (Proxy @s)) : levelNames (Proxy @ss)++type family Length (ss :: [Symbol]) :: Nat where+ Length '[] = 0+ Length (s ': ss) = 1 + Length ss++type family CheckLevels (n :: Nat) :: Constraint where+ CheckLevels 0 = TypeError ('Text "Score requires 2 to 10 levels")+ CheckLevels 1 = TypeError ('Text "Score requires 2 to 10 levels")+ CheckLevels 2 = ()+ CheckLevels 3 = ()+ CheckLevels 4 = ()+ CheckLevels 5 = ()+ CheckLevels 6 = ()+ CheckLevels 7 = ()+ CheckLevels 8 = ()+ CheckLevels 9 = ()+ CheckLevels 10 = ()+ CheckLevels n = TypeError ('Text "Score requires 2 to 10 levels; got " ':<>: 'ShowType n)++-- | Schema-independent metadata, also consumed by the generative evidence path.+data JudgmentKind+ = JudgeBoolean !(Maybe Value) !(Maybe Value)+ | JudgeChoice ![(Text, Maybe Value)]+ | JudgeOrdered ![Value]+ deriving stock (Eq, Show, Generic)++data Judgment = Judgment+ {judgmentField :: !Text, judgmentInstruction :: !(Maybe Text), judgmentKind :: !JudgmentKind}+ deriving stock (Eq, Show, Generic)++class IsDecision a where+ decisionKind :: Proxy a -> JudgmentKind+ decisionDescription :: Proxy a -> Maybe Text+ decisionDescription _ = Nothing++instance (KnownSymbol t, KnownSymbol f) => IsDecision (NoulWith t f) where+ decisionKind _ = JudgeBoolean (criterion @t) (criterion @f)++instance IsDecision Bool where decisionKind _ = JudgeBoolean Nothing Nothing++instance (ChoiceOptions a) => IsDecision (Choice a) where+ decisionKind _ = JudgeChoice [(n, String <$> d) | (n, _, d) <- choiceOptions @a]++instance (KnownLevels ss, CheckLevels (Length ss)) => IsDecision (Score ss) where+ decisionKind _ = JudgeOrdered (map String (levelNames (Proxy @ss)))++instance (KnownSymbol d, IsDecision a) => IsDecision (Field d a) where+ decisionKind _ = decisionKind (Proxy @a)+ decisionDescription _ = Just (T.pack (symbolVal (Proxy @d)))++criterion :: forall s. (KnownSymbol s) => Maybe Value+criterion = case T.pack (symbolVal (Proxy @s)) of+ "" -> Nothing+ t -> Just (String t)++-- | Compile-time guarantee that every output is a supported decision leaf.+class DecisionOutputs o where+ decisionJudgments :: Proxy o -> [Judgment]+ default decisionJudgments :: (GDecisionOutputs (Rep o)) => Proxy o -> [Judgment]+ decisionJudgments _ = gDecisionJudgments (Proxy @(Rep o))++judgmentsOf :: forall o. (DecisionOutputs o) => [Judgment]+judgmentsOf = decisionJudgments (Proxy @o)++type family RequireDecision (name :: Symbol) a :: Constraint where+ RequireDecision name (Field d a) = RequireDecision name a+ RequireDecision name (NoulWith t f) = ()+ RequireDecision name (Choice a) = ()+ RequireDecision name (Score ss) = ()+ RequireDecision name Bool = ()+ RequireDecision name a = TypeError ('Text "Decision output field `" ':<>: 'Text name ':<>: 'Text "` has unsupported type " ':<>: 'ShowType a)++class GDecisionOutputs (f :: Type -> Type) where+ gDecisionJudgments :: Proxy f -> [Judgment]++instance (GDecisionOutputs f) => GDecisionOutputs (D1 d f) where+ gDecisionJudgments _ = gDecisionJudgments (Proxy @f)++instance (GDecisionOutputs f) => GDecisionOutputs (C1 c f) where+ gDecisionJudgments _ = gDecisionJudgments (Proxy @f)++instance (GDecisionOutputs a, GDecisionOutputs b) => GDecisionOutputs (a :*: b) where+ gDecisionJudgments _ = gDecisionJudgments (Proxy @a) ++ gDecisionJudgments (Proxy @b)++instance (KnownSymbol name, RequireDecision name a, IsDecision a) => GDecisionOutputs (S1 ('MetaSel ('Just name) su ss ds) (K1 R a)) where+ gDecisionJudgments _ = [Judgment (T.pack (symbolVal (Proxy @name))) (decisionDescription (Proxy @a)) (decisionKind (Proxy @a))]++instance (KnownSymbol t, KnownSymbol f) => ToSchema (NoulWith t f) where+ toSchema _ = case decisionKind (Proxy @(NoulWith t f)) of+ JudgeBoolean Nothing Nothing -> marked "noul" ["type" .= ("boolean" :: Text)]+ JudgeBoolean t f -> marked "noul" ["type" .= ("boolean" :: Text), "anyOf" .= [branch (Bool True) t, branch (Bool False) f]]+ _ -> marked "noul" []++instance (ChoiceOptions a) => ToSchema (Choice a) where+ toSchema _ = marked "choice" ["type" .= ("string" :: Text), "anyOf" .= [branch (String n) (String <$> d) | (n, _, d) <- choiceOptions @a]]++instance (KnownLevels ss, CheckLevels (Length ss)) => ToSchema (Score ss) where+ toSchema _ = marked "score" ["type" .= ("integer" :: Text), "anyOf" .= [branch (toJSON i) (Just (String d)) | (i, d) <- zip [0 :: Int ..] (levelNames (Proxy @ss))]]++marked :: Text -> [Pair] -> Value+marked kind fields = object (fields ++ ["x-shikumi-decision" .= object ["kind" .= kind]])++branch :: Value -> Maybe Value -> Value+branch c d = object ["const" .= c, "description" .= d]++-- | Read only the declared judgment grammar. Unsupported or malformed fields+-- are returned separately for a pre-dispatch refusal. Property order follows a+-- complete, unique required array, otherwise ascending property name.+judgmentsInSchema :: Value -> ([Judgment], [Text])+judgmentsInSchema (Object schema)+ | Just (Object props) <- KM.lookup "properties" schema =+ foldMap (readField props) (propertyOrder schema props)+ where+ readField props name = case KM.lookup (Key.fromText name) props >>= readJudgment name of+ Just j -> ([j], [])+ Nothing -> ([], [name])+judgmentsInSchema _ = ([], [])++propertyOrder :: KM.KeyMap Value -> KM.KeyMap Value -> [Text]+propertyOrder schema props =+ let names = map (Key.toText . fst) (KM.toAscList props)+ in case KM.lookup "required" schema of+ Just (Array xs)+ | Just required <- traverse textOf (V.toList xs), sort required == names -> required+ _ -> names++readJudgment :: Text -> Value -> Maybe Judgment+readJudgment name (Object prop) = do+ -- Ignoring an unfamiliar constraint would widen the caller's declared domain.+ if all (`elem` ["type", "description", "title", "x-shikumi-decision", "anyOf", "oneOf", "enum"]) (KM.keys prop)+ then pure ()+ else Nothing+ instruction <- case KM.lookup "description" prop of+ Nothing -> Just Nothing+ Just Null -> Just Nothing+ Just (String t) -> Just (Just t)+ _ -> Nothing+ kind <- booleanKind prop <|> choiceKind prop <|> scoreKind prop+ pure (Judgment name instruction kind)+readJudgment _ _ = Nothing++booleanKind :: KM.KeyMap Value -> Maybe JudgmentKind+booleanKind p+ | KM.member "enum" p = Nothing+ | KM.lookup "type" p == Just (String "boolean"), not (KM.member "anyOf" p || KM.member "oneOf" p) = Just (JudgeBoolean Nothing Nothing)+ | allowedType "boolean" p = do+ if KM.member "anyOf" p && KM.member "oneOf" p then Nothing else pure ()+ Array xs <- KM.lookup "anyOf" p <|> KM.lookup "oneOf" p+ pairs <- traverse readBranch (V.toList xs)+ if sort (map fst pairs) == [Bool False, Bool True]+ then Just (JudgeBoolean (lookup (Bool True) pairs >>= id) (lookup (Bool False) pairs >>= id))+ else Nothing+ | otherwise = Nothing++choiceKind :: KM.KeyMap Value -> Maybe JudgmentKind+choiceKind p = do+ if allowedType "string" p && not (KM.member "oneOf" p) then pure () else Nothing+ labels <- case (KM.lookup "enum" p, KM.lookup "anyOf" p) of+ (Just (Array xs), Nothing) -> traverse (\v -> (,Nothing) <$> textOf v) (V.toList xs)+ (Nothing, Just (Array xs)) -> traverse (\v -> do (c, d) <- readBranch v; n <- textOf c; pure (n, d)) (V.toList xs)+ _ -> Nothing+ let names = map fst labels+ if null names || length names > 255 || Map.size (Map.fromList labels) /= length names+ then Nothing+ else Just (JudgeChoice labels)++scoreKind :: KM.KeyMap Value -> Maybe JudgmentKind+scoreKind p = do+ if allowedType "integer" p && not (KM.member "enum" p || KM.member "oneOf" p) then pure () else Nothing+ Array xs <- KM.lookup "anyOf" p+ pairs <- traverse readBranch (V.toList xs)+ let n = length pairs+ if n >= 2 && n <= 10 && map fst pairs == map toJSON [0 :: Int .. n - 1]+ then Just (JudgeOrdered (map (fromMaybe Null . snd) pairs))+ else Nothing++allowedType :: Text -> KM.KeyMap Value -> Bool+allowedType t p = maybe True (== String t) (KM.lookup "type" p)++readBranch :: Value -> Maybe (Value, Maybe Value)+readBranch (Object o)+ | all (`elem` ["const", "description"]) (KM.keys o) = do+ c <- KM.lookup "const" o+ pure (c, case KM.lookup "description" o of Just Null -> Nothing; d -> d)+readBranch _ = Nothing++textOf :: Value -> Maybe Text+textOf (String t) = Just t+textOf _ = Nothing++-- | Render a provider-neutral judgment question; no private marker is needed.+questionJSON :: Judgment -> Value+questionJSON (Judgment name instruction kind) =+ object+ (["type" .= kindName, "instructions" .= fromMaybe name instruction] ++ criteria)+ where+ (kindName, criteria) = case kind of+ JudgeBoolean Nothing Nothing -> ("noul" :: Text, [])+ JudgeBoolean t f -> ("noul", ["criteria" .= object ["true" .= t, "false" .= f]])+ JudgeChoice xs -> ("choice", ["criteria" .= object [Key.fromText n .= d | (n, d) <- xs]])+ JudgeOrdered ds -> ("score", ["criteria" .= ds])++-- | A single JSON state, with flat input/output demos. Demo answers use their+-- derived values through 'PromptValue', rather than evidence or Haskell Show.+renderDecisionState :: (ToPrompt i, ToPrompt o) => Signature i o -> i -> Value+renderDecisionState sig i =+ object+ ( [ "instructions" .= getInstruction sig,+ "input_fields" .= T.intercalate "\n" (zipWith describe [1 :: Int ..] (inputFields sig)),+ "inputs" .= toPromptJSON i+ ]+ ++ ["demos" .= map demo (getDemos sig) | not (null (getDemos sig))]+ )+ where+ inputObject = case toPromptJSON i of Object o -> o; _ -> KM.empty+ describe n (FieldMeta name description) =+ T.pack (show n) <> ". `" <> name <> "` (" <> jsonType (KM.lookup (Key.fromText name) inputObject) <> "): " <> fromMaybe "" description+ demo (Demo di o) = case (toPromptJSON di, toPromptJSON o) of+ (Object ins, Object outs) -> Object (KM.union outs ins)+ _ -> object []+ jsonType = \case+ Just (String _) -> "str"+ Just (Bool _) -> "bool"+ Just (Number _) -> "number"+ Just (Array _) -> "list"+ Just (Object _) -> "dict"+ _ -> "null"++-- Pure derivation accepts explicit settings; defaults are selected by decoding.+deriveNoul :: Double -> NoulEvidence -> (Bool, Double)+deriveNoul threshold (NoulEvidence p) = (p >= threshold, if denominator == 0 then 0 else abs (p - threshold) / denominator)+ where+ denominator = max threshold (1 - threshold)++deriveScore :: [Double] -> ScoreEvidence -> Either Text (Double, Int)+deriveScore cuts (ScoreEvidence ps _) = do+ validateMass (V.toList ps)+ let v = sum [fromIntegral n * p | (n, p) <- zip [0 :: Int ..] (V.toList ps)] / V.sum ps+ pure (v, length (filter (<= v) cuts))++-- | Select from declared labels in order; ties prefer the raw winner, then+-- declaration order. A probability map alone cannot retain that order.+deriveChoice :: [Text] -> Map Text Double -> ChoiceEvidence -> Either Text Text+deriveChoice labels weights (ChoiceEvidence ps _) = do+ if sort labels == Map.keys ps then pure () else Left "probability keys must match declared choice labels exactly"+ validateMass (Map.elems ps)+ let scored = [(label, ps Map.! label * Map.findWithDefault 1 label weights) | label <- labels]+ if any (\(_, p) -> not (finite p) || p < 0) scored+ then Left "weighted probabilities must be finite and non-negative"+ else if all ((== 0) . snd) scored then Left "no remaining probability mass" else pure ()+ let winner pairs = case pairs of+ [] -> Left "empty choice"+ x : xs -> Right (fst (foldl (\best next -> if snd next > snd best then next else best) x xs))+ raw <- winner [(label, ps Map.! label) | label <- labels]+ weighted <- winner scored+ pure (if lookup raw scored == lookup weighted scored then raw else weighted)++validateMass :: [Double] -> Either Text ()+validateMass ps+ | any (\p -> not (finite p) || p < 0 || p > 1) ps = Left "probabilities must be finite and in [0,1]"+ | sum ps <= 0 = Left "no remaining probability mass"+ | otherwise = Right ()++decodeEvidence :: (FromJSON a) => FieldPath -> Value -> Either ShikumiError a+decodeEvidence path = first (SchemaMismatch . ((renderPath path <> ": ") <>) . T.pack) . parseEither parseJSON++-- Internal normalized decode values, supplied after local derivation. These+-- annotations are never accepted from a provider evidence object.+derivedAt :: (FromJSON a) => FieldPath -> Value -> a -> Either ShikumiError a+derivedAt path (Object o) fallback = maybe (Right fallback) (decodeEvidence path) (KM.lookup "x-shikumi-derived" o)+derivedAt _ _ fallback = Right fallback++instance FromModel (NoulWith t f) where+ fromModelP path v = do+ e@(NoulEvidence p) <- decodeEvidence path v+ let (answer, c) = deriveNoul 0.5 e+ (answer', c') <- derivedAt path v (answer, c)+ pure (Noul answer' p c')++instance (ChoiceOptions a) => FromModel (Choice a) where+ fromModelP path v = do+ e@(ChoiceEvidence ps c) <- decodeEvidence path v+ label <- first (SchemaMismatch . ((renderPath path <> ": ") <>)) (deriveChoice [n | (n, _, _) <- choiceOptions @a] Map.empty e)+ label' <- derivedAt path v label+ case [a | (n, a, _) <- choiceOptions @a, n == label'] of+ a : _ -> pure (Choice a ps c)+ [] -> Left (SchemaMismatch (renderPath path <> ": undeclared choice label"))++instance (KnownLevels ss, CheckLevels (Length ss)) => FromModel (Score ss) where+ fromModelP path v = do+ e@(ScoreEvidence ps c) <- decodeEvidence path v+ let n = length (levelNames (Proxy @ss))+ if V.length ps == n then pure () else Left (SchemaMismatch (renderPath path <> ": score level count does not match declaration"))+ (answer, l) <- first (SchemaMismatch . ((renderPath path <> ": ") <>)) (deriveScore [fromIntegral k + 0.5 | k <- [0 .. n - 2]] e)+ (answer', l') <- derivedAt path v (answer, l)+ pure (Score answer' l' ps c)++instance ToJSON (NoulWith t f) where toJSON (Noul _ p _) = toJSON (NoulEvidence p)++instance ToJSON (Choice a) where toJSON (Choice _ ps c) = toJSON (ChoiceEvidence ps c)++instance ToJSON (Score ss) where toJSON (Score _ _ ps c) = toJSON (ScoreEvidence ps c)++instance Validatable (NoulWith t f)++instance Validatable (Choice a)++instance Validatable (Score ss)++instance PromptValue (NoulWith t f) where+ promptValue (Noul v _ _) = if v then "true" else "false"+ promptValueJSON (Noul v _ _) = Bool v+ promptEvidenceJSON n@(Noul v _ c) = case toJSON n of Object o -> Object (KM.insert "value" (Bool v) (KM.insert "confidence" (toJSON c) o)); x -> x++instance (ChoiceOptions a) => PromptValue (Choice a) where+ promptValue (Choice a _ _) = fromMaybe "" (lookup a [(v, n) | (n, v, _) <- choiceOptions @a])+ promptValueJSON = String . promptValue+ promptEvidenceJSON c = case toJSON c of Object o -> Object (KM.insert "value" (String (promptValue c)) o); x -> x++instance PromptValue (Score ss) where+ promptValue (Score _ l _ _) = T.pack (show l)+ promptValueJSON (Score _ l _ _) = toJSON l+ promptEvidenceJSON s@(Score v l _ _) = case toJSON s of Object o -> Object (KM.insert "value" (toJSON v) (KM.insert "level" (toJSON l) o)); x -> x
+ src/Shikumi/Decision/Runtime.hs view
@@ -0,0 +1,98 @@+-- | Extraction of provider-neutral probability evidence. Provider selections are+-- deliberately ignored; the typed decoder derives every answer locally.+module Shikumi.Decision.Runtime (evidenceOf, evidenceObject, decodeDecisionEvidence) where++import Baikai (AssistantContent (..), Response)+import Control.Lens ((^.))+import Data.Aeson (Result (..), Value (..), fromJSON)+import Data.Aeson.Key qualified as K+import Data.Aeson.KeyMap qualified as KM+import Data.Generics.Labels ()+import Data.List (sort)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Data.Vector qualified as V+import Shikumi.Decision+import Shikumi.Decision.Setting (DecisionSetting, deriveField)+import Shikumi.Error (ShikumiError (..))++evidenceOf :: [Judgment] -> Response -> Maybe (Either ShikumiError (Map Text FieldEvidence))+evidenceOf js response = case [d | AssistantData d <- V.toList (response ^. #message . #content)] of+ [] -> Nothing+ [d] -> Just $ do+ ps <- maybe (Left (SchemaMismatch "decision response: missing probabilities")) Right (d ^. #probabilities)+ let cs = maybe Map.empty id (d ^. #confidence)+ get field key probs = maybe (Left (SchemaMismatch (field <> ": missing probability " <> key))) Right (Map.lookup key probs)+ Map.fromList+ <$> traverse+ ( \j -> do+ let name = judgmentField j+ probs <- maybe (Left (MissingField name)) Right (Map.lookup name ps)+ if all (\p -> not (isNaN p || isInfinite p) && p >= 0 && p <= 1) (Map.elems probs ++ maybe [] pure (Map.lookup name cs))+ then pure ()+ else Left (ValidationFailure (name <> ": probability/confidence must be finite and in [0,1]"))+ ev <- case judgmentKind j of+ JudgeBoolean {} -> FieldNoul . NoulEvidence <$> get name "true" probs+ JudgeChoice labels -> do+ if Map.keysSet probs == Map.keysSet (Map.fromList labels) then pure () else Left (SchemaMismatch (name <> ": probability keys differ from declared labels"))+ pure (FieldChoice (ChoiceEvidence probs (Map.lookup name cs)))+ JudgeOrdered levels -> do+ let keys = map (T.pack . show) [0 .. length levels - 1]+ if Map.keysSet probs == Map.keysSet (Map.fromList [(k, ()) | k <- keys]) then pure () else Left (SchemaMismatch (name <> ": probability keys differ from declared levels"))+ values <- traverse (\k -> get name k probs) keys+ pure (FieldScore (ScoreEvidence (V.fromList values) (Map.lookup name cs)))+ pure (name, ev)+ )+ js+ _ -> Just (Left (SchemaMismatch "decision response: expected one structured data block"))++-- | Rich fields decode evidence; native closed fields decode the derived scalar.+evidenceObject :: Value -> [Judgment] -> Map Text DecisionSetting -> Map Text FieldEvidence -> Either ShikumiError Value+evidenceObject schema js settings evidence = Object . KM.fromList <$> traverse field js+ where+ props = case schema of+ Object o | Just (Object p) <- KM.lookup "properties" o -> p+ _ -> KM.empty+ field j = do+ let name = judgmentField j+ rich = case KM.lookup (K.fromText name) props of Just (Object p) -> KM.member "x-shikumi-decision" p; _ -> False+ ev <- maybe (Left (MissingField name)) Right (Map.lookup name evidence)+ derived <- deriveField j (Map.lookup name settings) ev+ let native = case derived of+ Object o | Just result <- KM.lookup "x-shikumi-derived" o -> case judgmentKind j of+ JudgeBoolean {} -> case result of Array a -> maybe Null id (a V.!? 0); _ -> Null+ JudgeOrdered {} -> case result of Array a -> maybe Null id (a V.!? 1); _ -> Null+ JudgeChoice {} -> result+ _ -> derived+ pure (K.fromText name, if rich then derived else native)++-- | Strict generative evidence grammar. Shape errors and range violations keep+-- the field path. No provider-supplied derived-answer annotation is accepted.+decodeDecisionEvidence :: Judgment -> Value -> Either ShikumiError FieldEvidence+decodeDecisionEvidence j value = do+ fields <- case value of Object o -> Right o; _ -> shape "expected evidence object"+ let expected = case judgmentKind j of JudgeBoolean {} -> ["noul"]; _ -> ["confidence", "probabilities"]+ if sort (KM.keys fields) == sort expected then pure () else shape "missing or extra evidence keys"+ case judgmentKind j of+ JudgeBoolean {} -> FieldNoul . NoulEvidence <$> (get "noul" fields >>= number)+ kind -> do+ probs <- get "probabilities" fields >>= \case Object p -> Right p; _ -> shape "expected probabilities object"+ let keys = case kind of JudgeChoice ls -> map (K.fromText . fst) ls; JudgeOrdered ls -> map (K.fromText . T.pack . show) [0 .. length ls - 1]; _ -> []+ if sort (KM.keys probs) == sort keys then pure () else shape "missing or extra probability keys"+ ps <- traverse number probs+ confidence <- get "confidence" fields >>= number+ let mapping = Map.fromList [(K.toText key, p) | (key, p) <- KM.toList ps]+ case kind of+ JudgeChoice {} -> pure (FieldChoice (ChoiceEvidence mapping (Just confidence)))+ JudgeOrdered {} -> pure (FieldScore (ScoreEvidence (V.fromList [mapping Map.! K.toText key | key <- keys]) (Just confidence)))+ _ -> shape "unexpected judgment kind"+ where+ shape :: Text -> Either ShikumiError a+ shape msg = Left (SchemaMismatch (judgmentField j <> ": " <> msg))+ get key fields = maybe (shape ("missing " <> K.toText key)) Right (KM.lookup key fields)+ number (Number n) = case fromJSON (Number n) of+ Success p | not (isNaN p || isInfinite p) && p >= 0 && p <= (1 :: Double) -> Right p+ _ -> Left (ValidationFailure (judgmentField j <> ": probability/confidence must be finite and in [0,1]"))+ number _ = shape "expected numeric probability/confidence"
+ src/Shikumi/Decision/Setting.hs view
@@ -0,0 +1,85 @@+-- | Pure per-field calibration settings and local derivation. These values never+-- enter provider requests or response-cache identity.+module Shikumi.Decision.Setting (DecisionSetting (..), defaultSetting, validateSetting, deriveField) where++import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, toJSON, withObject, (.:), (.=))+import Data.Aeson.KeyMap qualified as KM+import Data.Aeson.Types (Parser)+import Data.Bifunctor (first)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import GHC.Generics (Generic)+import Shikumi.Decision (Judgment (..), JudgmentKind (..))+import Shikumi.Decision qualified as D+import Shikumi.Error (ShikumiError (..))++data DecisionSetting+ = NoulSetting {threshold :: !Double}+ | ScoreSetting {cuts :: ![Double]}+ | ChoiceSetting {weights :: !(Map Text Double)}+ deriving stock (Eq, Show, Generic)++instance ToJSON DecisionSetting where+ toJSON = \case+ NoulSetting t -> object ["kind" .= ("noul" :: Text), "threshold" .= t]+ ScoreSetting cs -> object ["kind" .= ("score" :: Text), "cuts" .= cs]+ ChoiceSetting ws -> object ["kind" .= ("choice" :: Text), "weights" .= ws]++instance FromJSON DecisionSetting where+ parseJSON = withObject "DecisionSetting" $ \o -> do+ kind <- o .: "kind" :: Parser Text+ case kind of+ "noul" -> NoulSetting <$> o .: "threshold"+ "score" -> ScoreSetting <$> o .: "cuts"+ "choice" -> ChoiceSetting <$> o .: "weights"+ _ -> fail "unknown decision setting kind"++defaultSetting :: Judgment -> DecisionSetting+defaultSetting j = case judgmentKind j of+ JudgeBoolean {} -> NoulSetting 0.5+ JudgeOrdered levels -> ScoreSetting [fromIntegral k + 0.5 | k <- [0 .. length levels - 2]]+ JudgeChoice {} -> ChoiceSetting Map.empty++-- | Validate against the declared domain. Omitted Choice weights stay absent+-- and mean 1; zero weights are valid and disable the corresponding label.+validateSetting :: Judgment -> DecisionSetting -> Either Text DecisionSetting+validateSetting j setting = case (judgmentKind j, setting) of+ (JudgeBoolean {}, NoulSetting t)+ | not (finite t) || t < 0 || t > 1 -> bad "threshold must be finite and in [0,1]"+ | otherwise -> Right setting+ (JudgeOrdered levels, ScoreSetting cs)+ | length cs /= length levels - 1 -> bad "cuts must have exactly N-1 entries"+ | any (\c -> not (finite c) || c <= 0 || c >= fromIntegral (length levels - 1)) cs -> bad "cuts must be finite and in (0,N-1)"+ | not (and (zipWith (<) cs (drop 1 cs))) -> bad "cuts must be strictly increasing"+ | otherwise -> Right setting+ (JudgeChoice options, ChoiceSetting ws)+ | any (`notElem` map fst options) (Map.keys ws) -> bad "weights must name declared labels"+ | any (\w -> not (finite w) || w < 0) (Map.elems ws) -> bad "weights must be finite and non-negative"+ | otherwise -> Right setting+ _ -> bad "setting kind does not match the decision field"+ where+ bad rule = Left (judgmentField j <> ": " <> rule)+ finite x = not (isNaN x || isInfinite x)++-- | Derive once, after validating settings and evidence. The private annotation+-- is consumed only by the rich FromModel leaves; it never enters request identity+-- or the public evidence codecs. Native fields receive an ordinary scalar.+deriveField :: Judgment -> Maybe DecisionSetting -> D.FieldEvidence -> Either ShikumiError Value+deriveField j stored ev = do+ setting <- first ValidationFailure (validateSetting j (fromMaybe (defaultSetting j) stored))+ case (judgmentKind j, setting, ev) of+ (JudgeBoolean {}, NoulSetting t, D.FieldNoul e@(D.NoulEvidence _)) ->+ let answer = D.deriveNoul t e in pure (annotate (toJSON answer) (toJSON e))+ (JudgeChoice labels, ChoiceSetting ws, D.FieldChoice e) -> do+ label <- first located (D.deriveChoice (map fst labels) ws e)+ pure (annotate (toJSON label) (toJSON e))+ (JudgeOrdered _, ScoreSetting cs, D.FieldScore e) -> do+ answer <- first located (D.deriveScore cs e)+ pure (annotate (toJSON answer) (toJSON e))+ _ -> Left (located "evidence kind does not match judgment")+ where+ located msg = SchemaMismatch (judgmentField j <> ": " <> msg)+ annotate result (Object o) = Object (KM.insert "x-shikumi-derived" result o)+ annotate _ v = v
src/Shikumi/LLM/Defaults.hs view
@@ -11,6 +11,7 @@ import Baikai (Options) import Baikai.Evidence (EvidenceRequest)+import Baikai.Model (isJudgmentOnly) import Baikai.Speed (Speed) import Baikai.ThinkingLevel (ThinkingLevel) import Control.Applicative ((<|>))@@ -57,9 +58,12 @@ withRequestDefaults defaults action = do when (defaultMaxTokens defaults == Just 0) $ throwError (ValidationFailure "request defaults: defaultMaxTokens must be positive")+ let forModel model+ | isJudgmentOnly model = emptyRequestDefaults {defaultEvidence = defaultEvidence defaults}+ | otherwise = defaults interpose ( \_ -> \case- Complete model ctx opts -> complete model ctx (applyRequestDefaults defaults opts)- Stream model ctx opts -> stream model ctx (applyRequestDefaults defaults opts)+ Complete model ctx opts -> complete model ctx (applyRequestDefaults (forModel model) opts)+ Stream model ctx opts -> stream model ctx (applyRequestDefaults (forModel model) opts) ) action
src/Shikumi/Module.hs view
@@ -31,7 +31,7 @@ import Baikai (emptyContext, emptyModel, emptyOptions, user) import Control.Lens ((&), (.~))-import Data.Aeson (Object, ToJSON (..), Value (Object))+import Data.Aeson (Object, ToJSON (..), Value (Object, String)) import Data.Aeson.Key qualified as Key import Data.Aeson.KeyMap qualified as KM import Data.Generics.Labels ()@@ -115,6 +115,10 @@ -- Render without leaning on @o@'s @Show@: reuse @o@'s own t'ToPrompt'. instance (ToPrompt o) => ToPrompt (WithReasoning o) where toPromptFields wr = ("reasoning", reasoning wr) : toPromptFields (value wr)+ toEvidenceJSON = toPromptJSON+ toPromptJSON wr = case toPromptJSON (value wr) of+ Object fields -> Object (KM.insert "reasoning" (String (reasoning wr)) fields)+ v -> v -- @value@ is polymorphic in @o@, so the generic image walk cannot resolve; a -- t'WithReasoning' is a text-only output wrapper and carries no image fields.
src/Shikumi/Program.hs view
@@ -47,6 +47,10 @@ Params (..), Demo (..), emptyParams,+ decisionSettingFor,+ setDecisionSetting,+ clearDecisionSetting,+ parseResponseWithSettings, TempSchedule (..), -- * Construction & execution@@ -58,6 +62,7 @@ -- * Per-node field metadata (EP-16) NodeFields (..), nodeFieldsIndexed,+ nodeDecisionFieldsIndexed, nodeInstructionsIndexed, -- * Execution internals (reused by alternative executors, e.g. @runProgramTraced@)@@ -85,11 +90,15 @@ where import Baikai (Model, emptyModel)-import Data.Aeson (FromJSON, ToJSON, Value)+import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withObject, (.!=), (.:), (.:?), (.=))+import Data.Aeson.Key qualified as Key+import Data.Aeson.KeyMap qualified as KM+import Data.Bifunctor (first) import Data.Functor.Const (Const (..)) import Data.Functor.Identity (Identity (..)) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NE+import Data.Map.Strict qualified as Settings import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as T@@ -104,16 +113,24 @@ ToPrompt, adapterFor, assistantJSON,+ attachDecisionState, attachNativeRender, attachSchema,+ evidenceFields,+ evidenceSchema, fallbackAdapter, nativeAdapter, nativeRenderPieces,+ renderEvidenceRequest,+ responseValue, stampTemperature, )+import Shikumi.Decision (Judgment (..), judgmentsInSchema, renderDecisionState)+import Shikumi.Decision.Runtime (decodeDecisionEvidence, evidenceObject, evidenceOf)+import Shikumi.Decision.Setting (DecisionSetting, validateSetting) import Shikumi.Error (ShikumiError (..)) import Shikumi.LLM (LLM (..), Response, complete)-import Shikumi.Schema (FromModel, ToSchema, Validatable, deriveSchema, fromModel)+import Shikumi.Schema (FromModel, ToSchema, Validatable, deriveSchema, fromModel, fromModelChecked) import Shikumi.Schema.Types (fieldName) import Shikumi.Signature (Signature, getInstruction, inputFields, outputFields, setDemos, setInstruction) import Shikumi.Signature qualified as Sig@@ -130,14 +147,29 @@ -- @i@/@o@ — hence demos are stored as type-agnostic JSON (see t'Demo'). data Params = Params { instructionOverride :: !(Maybe Text),- demos :: ![Demo]+ demos :: ![Demo],+ decisionSettings :: !(Settings.Map Text DecisionSetting) } deriving stock (Eq, Show, Generic) -instance ToJSON Params+instance ToJSON Params where+ toJSON ps =+ object $+ ["instructionOverride" .= instructionOverride ps, "demos" .= demos ps]+ ++ ["decisionSettings" .= decisionSettings ps | not (Settings.null (decisionSettings ps))] -instance FromJSON Params+instance FromJSON Params where+ parseJSON = withObject "Params" $ \o -> Params <$> o .: "instructionOverride" <*> o .: "demos" <*> o .:? "decisionSettings" .!= Settings.empty +decisionSettingFor :: Text -> Params -> Maybe DecisionSetting+decisionSettingFor name = Settings.lookup name . decisionSettings++setDecisionSetting :: Text -> DecisionSetting -> Params -> Params+setDecisionSetting name setting ps = ps {decisionSettings = Settings.insert name setting (decisionSettings ps)}++clearDecisionSetting :: Text -> Params -> Params+clearDecisionSetting name ps = ps {decisionSettings = Settings.delete name (decisionSettings ps)}+ -- | A worked input/output example, stored as JSON so it is uniform across nodes -- of differing types. At run time each demo is decoded back into the node's typed -- @Sig.Demo i o@ and spliced into the prompt by EP-3's adapter; a demo whose JSON@@ -154,7 +186,7 @@ -- | The default empty overlay: no instruction override, no demos. emptyParams :: Params-emptyParams = Params Nothing []+emptyParams = Params Nothing [] Settings.empty -- | How 'MajorityVote' varies sampling temperature across its @K@ samples. -- 'TempFixed' lists an explicit temperature per sample (cycled if shorter than@@ -338,9 +370,18 @@ i -> Eff es o runPredict sig ps i = do+ let fields = fst (judgmentsInSchema (deriveSchema @o))+ either throwError pure $+ mapM_+ ( \(name, setting) ->+ case filter ((== name) . judgmentField) fields of+ [j] -> () <$ first ValidationFailure (validateSetting j setting)+ _ -> Left (ValidationFailure (name <> ": unknown decision field"))+ )+ (Settings.toList (decisionSettings ps)) sig' <- effectiveSignature sig ps let adapter = adapterFor placeholderModel- (ctx, opts0) = render adapter sig' i+ (ctx0, opts0) = render adapter sig' i -- Stamp the derived schema onto the metadata channel regardless of which -- adapter rendered the prompt; the router attaches a real @responseFormat@ -- from it for native-capable models and strips it otherwise.@@ -350,9 +391,15 @@ -- swaps in these native pieces once it knows the real model is native-capable -- (and strips them for fallback models / un-routed runs). (nativeSys, nativeDemos) = nativeRenderPieces @i @o sig'- opts = attachNativeRender nativeSys nativeDemos (attachSchema (deriveSchema @o) opts0)+ schema = deriveSchema @o+ (ctx, renderedOpts)+ | null (evidenceFields @o (decisionSettings ps)) = (ctx0, attachNativeRender nativeSys nativeDemos (attachSchema schema opts0))+ | otherwise = renderEvidenceRequest (decisionSettings ps) sig' i+ opts+ | null (fst (judgmentsInSchema schema)) = renderedOpts+ | otherwise = attachDecisionState (renderDecisionState sig' i) renderedOpts resp <- complete placeholderModel ctx opts- either throwError pure (parseResponse sig' resp)+ either throwError pure (parseResponseWithSettings (decisionSettings ps) sig' resp) -- | Decode a response back into the typed output, tolerant of /either/ wire shape: -- a native model (whose @responseFormat@ the router enforced) replies with a JSON@@ -375,13 +422,38 @@ Signature i o -> Response -> Either ShikumiError o-parseResponse sig' resp =- case assistantJSON resp of- -- The body is JSON: the native wire shape. Keep the native parser's error.- Right _ -> parse (nativeAdapter @i @o) sig' resp- -- Not JSON: the fallback / marker wire shape. Exact prior behaviour.- Left _ -> parse (fallbackAdapter @i @o) sig' resp+parseResponse = parseResponseWithSettings Settings.empty +parseResponseWithSettings :: forall i o. (FromModel o, ToSchema o, Validatable o, ToPrompt i, ToPrompt o) => Settings.Map Text DecisionSetting -> Signature i o -> Response -> Either ShikumiError o+parseResponseWithSettings settings sig' resp+ | Just ev <- evidenceOf (fst (judgmentsInSchema (deriveSchema @o))) resp =+ ev >>= evidenceObject (deriveSchema @o) (fst (judgmentsInSchema (deriveSchema @o))) settings >>= fromModelChecked+ | not (null selected) = do+ raw <- responseValue (evidenceSchema (deriveSchema @o) selected) resp+ evidence <- case raw of+ Object fields ->+ Settings.fromList+ <$> traverse+ ( \j -> do+ let name = judgmentField j+ value <- maybe (Left (MissingField name)) Right (KM.lookup (Key.fromText name) fields)+ ev <- decodeDecisionEvidence j value+ pure (name, ev)+ )+ selected+ _ -> Left (SchemaMismatch "decision response: expected object")+ normalized <- evidenceObject (deriveSchema @o) selected settings evidence+ case (normalized, raw) of+ (Object derived, Object fields) -> fromModelChecked (Object (KM.union derived fields))+ _ -> Left (SchemaMismatch "decision response: expected object")+ | otherwise = case assistantJSON resp of+ -- The body is JSON: the native wire shape. Keep the native parser's error.+ Right _ -> parse (nativeAdapter @i @o) sig' resp+ -- Not JSON: the fallback / marker wire shape. Exact prior behaviour.+ Left _ -> parse (fallbackAdapter @i @o) sig' resp+ where+ selected = evidenceFields @o settings+ -- | The per-sample temperatures for a 'MajorityVote' of @k@ samples (at least -- one — @k@ is clamped up to 1 here so the executors need not). 'Nothing' leaves -- the provider default in place; 'Just t' is stamped onto the sample's requests.@@ -533,6 +605,29 @@ go (PredictCaptured _ sig ps) = go (Predict sig ps) go (Predict sig _) = [NodeFields (map fieldName (inputFields sig)) (map fieldName (outputFields sig))]+ go (Compose a b) = go a ++ go b+ go (FMap _ p) = go p+ go (Map _ p) = go p+ go (Parallel a b) = go a ++ go b+ go (Retry _ p) = go p+ go (RetryWhen _ _ p) = go p+ go (Validate _ p) = go p+ go (MajorityVote _ _ _ p) = go p+ go (Ensemble ps _) = concatMap go ps+ go (Embed _) = []++-- | Rich decision fields in parameter traversal order. Native booleans and+-- enums remain ordinary generation and are not calibration targets.+nodeDecisionFieldsIndexed :: Program i o -> [[Judgment]]+nodeDecisionFieldsIndexed = go+ where+ go :: forall x y. Program x y -> [[Judgment]]+ go (PredictCaptured _ sig ps) = go (Predict sig ps)+ go (Predict (_ :: Signature x y) _) =+ let schema = deriveSchema @y+ props = case schema of Object o | Just (Object ps) <- KM.lookup "properties" o -> ps; _ -> KM.empty+ rich j = case KM.lookup (Key.fromText (judgmentField j)) props of Just (Object p) -> KM.member "x-shikumi-decision" p; _ -> False+ in [filter rich (fst (judgmentsInSchema schema))] go (Compose a b) = go a ++ go b go (FMap _ p) = go p go (Map _ p) = go p
src/Shikumi/Refine.hs view
@@ -50,7 +50,7 @@ where import Control.Lens ((&), (.~), (^.))-import Data.Aeson (Object, Value (Object))+import Data.Aeson (Object, Value (Object, String)) import Data.Aeson.Key qualified as Key import Data.Aeson.KeyMap qualified as KM import Data.Generics.Labels ()@@ -312,6 +312,8 @@ -- | Render the original input, then each attempt as a "Student Attempt #k" line: -- @"I tried to \<reasoning\>; my answer is \<answer\>"@ (mirroring DSPy's rendering). instance (ToPrompt i, ToPrompt o) => ToPrompt (MultiChainInput i o) where+ toEvidenceJSON = toPromptJSON+ toPromptJSON = Object . KM.fromList . map (\(k, v) -> (Key.fromText k, String v)) . toPromptFields toPromptFields mci = toPromptFields (original mci) ++ [ ("Student Attempt #" <> tshow k, renderAttempt a)
src/Shikumi/Routing.hs view
@@ -47,12 +47,14 @@ ) where -import Baikai (Context, JsonSchemaFormat (strict), Message (..), Model, Options, ResponseFormat (..), assistant, jsonSchemaFormat)+import Baikai (Context, JsonSchemaFormat (strict), Message (..), Model, Options, ResponseFormat (..), assistant, jsonSchemaFormat, userData)+import Control.Applicative ((<|>)) import Control.Lens ((&), (.~), (^.)) import Data.Aeson (FromJSON, Result (..), Value, fromJSON) import Data.Generics.Labels () import Data.Map.Strict qualified as Map import Data.Text (Text)+import Data.Text qualified as T import Data.Vector qualified as V import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, (:>)) import Effectful.Dispatch.Dynamic (interpose, interpret, send)@@ -60,12 +62,17 @@ import Shikumi.Adapter ( ModelCapability (..), capabilityFor,+ metaDecisionEvidenceKey,+ metaDecisionSchemaKey,+ metaDecisionStateKey, metaNativeDemosKey, metaNativePromptKey, metaResponseSchemaKey, metaTemperatureKey,+ stripDecisionMarkers, )-import Shikumi.Error (ShikumiError)+import Shikumi.Decision (judgmentsInSchema)+import Shikumi.Error (ShikumiError (..)) import Shikumi.LLM (LLM (..), complete, stream) import Shikumi.LLM.Continuation (validateRequestContinuation) @@ -99,12 +106,13 @@ routeLLM = interpose $ \_ -> \case Complete _placeholder ctx opts -> do m <- currentModel- let (ctx', opts') = translateForWire m ctx opts+ (ctx', opts') <- either throwError pure (translateForWire m ctx opts) either throwError pure (validateRequestContinuation m ctx' opts') complete m ctx' opts' Stream _placeholder ctx opts -> do m <- currentModel- let (ctx', opts') = translateForWire m ctx opts+ if capabilityFor m == DecisionOnly || Map.member metaDecisionEvidenceKey (opts ^. #metadata) then throwError (ValidationFailure "decision outputs do not support streaming") else pure ()+ (ctx', opts') <- either throwError pure (translateForWire m ctx opts) either throwError pure (validateRequestContinuation m ctx' opts') stream m ctx' opts' @@ -116,10 +124,10 @@ -- Continuation expectations remain available for later boundary checks. Fallback-capability models keep the -- marker t'Context' unchanged; non-'Shikumi.Program.Predict' @Complete@ calls (no stamps) are never -- rewritten.-translateForWire :: Model -> Context -> Options -> (Context, Options)+translateForWire :: Model -> Context -> Options -> Either ShikumiError (Context, Options) translateForWire m ctx opts = let md = opts ^. #metadata- mSchema = Map.lookup metaResponseSchemaKey md+ mSchema = (if capabilityFor m == DecisionOnly then Map.lookup metaDecisionSchemaKey md else Nothing) <|> Map.lookup metaResponseSchemaKey md mTemp = Map.lookup metaTemperatureKey md >>= valueToDouble mNativeSys = Map.lookup metaNativePromptKey md >>= fromJSONMaybe mNativeDemos = Map.lookup metaNativeDemosKey md >>= fromJSONMaybe@@ -127,12 +135,12 @@ foldr Map.delete md- [metaResponseSchemaKey, metaTemperatureKey, metaNativePromptKey, metaNativeDemosKey]- isNative = case capabilityFor m of NativeSchema -> True; PromptFallback -> False+ [metaResponseSchemaKey, metaTemperatureKey, metaNativePromptKey, metaNativeDemosKey, metaDecisionStateKey, metaDecisionSchemaKey, metaDecisionEvidenceKey]+ isNative = capabilityFor m /= PromptFallback withSchema o | isNative, Just s <- mSchema =- o & #responseFormat .~ Just (JsonSchema ((jsonSchemaFormat "output" s) {strict = True}))+ o & #responseFormat .~ Just (JsonSchema ((jsonSchemaFormat "output" (stripDecisionMarkers s)) {strict = True})) | otherwise = o withTemp o = case mTemp of Just t -> o & #temperature .~ Just t@@ -141,7 +149,15 @@ ctx' = case (isNative, mNativeSys, mNativeDemos) of (True, Just sys, Just demos) -> rewriteNativeContext sys demos ctx _ -> ctx- in (ctx', opts')+ in if capabilityFor m == DecisionOnly+ then case (mSchema, Map.lookup metaDecisionStateKey md) of+ (Just s, Just state)+ | let (js, free) = judgmentsInSchema s,+ not (null js),+ null free ->+ Right (ctx & #systemPrompt .~ Nothing & #tools .~ V.empty & #messages .~ V.singleton (userData state), opts')+ _ -> Left (ValidationFailure ((m ^. #modelId) <> " answers only declared judgments; free-form output fields: " <> maybe "missing schema/state" (T.intercalate ", " . snd . judgmentsInSchema) mSchema))+ else Right (ctx', opts') -- | Install the native-format system prompt and demo assistant turns into a -- t'Context'. The assistant turns of a 'Shikumi.Program.Predict'-rendered context are exactly the
src/Shikumi/Stream.hs view
@@ -55,6 +55,7 @@ emptyTextContent, ) import Control.Lens ((&), (.~))+import Control.Monad (when) import Data.Generics.Labels () import Data.Maybe (fromMaybe, listToMaybe) import Data.Text (Text)@@ -62,11 +63,11 @@ import Effectful (Eff, (:>)) import Effectful.Error.Static (Error, throwError) import GHC.Generics (Generic)-import Shikumi.Adapter (Adapter (..), ToPrompt, adapterFor, attachNativeRender, attachSchema, nativeRenderPieces)-import Shikumi.Error (ShikumiError)+import Shikumi.Adapter (Adapter (..), ToPrompt, adapterFor, attachNativeRender, attachSchema, evidenceFields, nativeRenderPieces)+import Shikumi.Error (ShikumiError (..)) import Shikumi.LLM (LLM, stream) import Shikumi.Program- ( Params,+ ( Params (..), Program (Compose, Embed, FMap, MajorityVote, Map, Parallel, Predict, PredictCaptured, Retry, RetryWhen, Validate), acceptOrReject, effectiveSignature,@@ -238,7 +239,7 @@ -- | Stream a single 'Predict', mirroring 'Shikumi.Program.runPredict' exactly so -- the streamed and blocking paths share one wire + decode definition. It overlays--- 'Params' via the exported 'effectiveSignature', renders model-agnostically, stamps+-- t'Params' via the exported 'effectiveSignature', renders model-agnostically, stamps -- the derived schema and the native render alternative onto the metadata channel -- (so the router can attach a native @responseFormat@ and swap the native prompt for -- native-capable models), streams the first output field's chunks bracketed by@@ -260,6 +261,7 @@ (StreamEvent -> Eff es ()) -> Eff es o streamPredict sig ps i cb = do+ when (not (null (evidenceFields @o (decisionSettings ps)))) (throwError (ValidationFailure "decision outputs do not support streaming")) sig' <- effectiveSignature sig ps let adapter = adapterFor emptyModel (ctx, opts0) = render adapter sig' i
+ test/DecisionDeriveSpec.hs view
@@ -0,0 +1,94 @@+module DecisionDeriveSpec (tests) where++import Data.Aeson (eitherDecode, encode, object, toJSON, (.=))+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Text qualified as T+import Data.Vector qualified as V+import Shikumi.Decision+import Shikumi.Decision.Setting+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))+import Test.Tasty.QuickCheck (choose, forAll, testProperty)++noul :: Judgment+noul = Judgment "urgent" Nothing (JudgeBoolean Nothing Nothing)++score :: Judgment+score = Judgment "severity" Nothing (JudgeOrdered [toJSON ("Minor" :: Text), toJSON ("Disruptive" :: Text), toJSON ("Blocking" :: Text)])++choice :: Judgment+choice = Judgment "category" Nothing (JudgeChoice [("z", Nothing), ("a", Nothing)])++tests :: TestTree+tests =+ testGroup+ "Decision derivation"+ [ testCase "Noul threshold 0.7 on p=0.8 gives True, confidence 0.142857" $ do+ let (answer, c) = deriveNoul 0.7 (NoulEvidence 0.8)+ answer @?= True+ assertBool "confidence" (abs (c - 1 / 7) < 1e-12),+ testCase "Noul endpoints and equality use an inclusive threshold" $ do+ deriveNoul 0 (NoulEvidence 0) @?= (True, 0)+ deriveNoul 1 (NoulEvidence 1) @?= (True, 0)+ deriveNoul 0.7 (NoulEvidence 0.7) @?= (True, 0),+ testCase "Score cuts change the level while preserving value" $ do+ let e = ScoreEvidence (V.fromList [0.1, 0.3, 0.6]) (Just 0.55)+ deriveScore [0.5, 1.6] e @?= Right (1.5, 1)+ deriveScore [0.5, 1.5] e @?= Right (1.5, 2),+ testCase "Choice raw tie follows declaration order" $+ deriveChoice ["z", "a"] Map.empty (ChoiceEvidence (Map.fromList [("z", 0.5), ("a", 0.5)]) Nothing) @?= Right "z",+ testCase "Choice weighted tie prefers the raw winner" $+ deriveChoice ["z", "a"] (Map.singleton "z" 4) (ChoiceEvidence (Map.fromList [("z", 0.2), ("a", 0.8)]) (Just 0.65)) @?= Right "a",+ testCase "zero-weight label is never selected" $+ deriveChoice ["z", "a"] (Map.singleton "z" 0) (ChoiceEvidence (Map.fromList [("z", 0.9), ("a", 0.1)]) Nothing) @?= Right "a",+ testCase "weights greater than one can change the winner" $+ deriveChoice ["z", "a"] (Map.singleton "z" 5) (ChoiceEvidence (Map.fromList [("z", 0.2), ("a", 0.8)]) Nothing) @?= Right "z",+ testCase "all-zero probability or weighted mass is rejected" $ do+ deriveChoice ["z", "a"] Map.empty (ChoiceEvidence (Map.fromList [("z", 0), ("a", 0)]) Nothing) @?= Left "no remaining probability mass"+ deriveChoice ["z", "a"] (Map.fromList [("z", 0), ("a", 0)]) (ChoiceEvidence (Map.fromList [("z", 0.5), ("a", 0.5)]) Nothing) @?= Left "no remaining probability mass"+ deriveScore [0.5] (ScoreEvidence (V.fromList [0, 0]) Nothing) @?= Left "no remaining probability mass",+ testCase "defaults match the declared kind and level count" $ do+ defaultSetting noul @?= NoulSetting 0.5+ defaultSetting score @?= ScoreSetting [0.5, 1.5]+ defaultSetting choice @?= ChoiceSetting Map.empty,+ testCase "settings have stable tagged JSON and round-trip" $ do+ let settings = [NoulSetting 0.7, ScoreSetting [0.5, 1.6], ChoiceSetting (Map.singleton "z" 2)]+ map toJSON settings+ @?= [object ["kind" .= ("noul" :: Text), "threshold" .= (0.7 :: Double)], object ["kind" .= ("score" :: Text), "cuts" .= ([0.5, 1.6] :: [Double])], object ["kind" .= ("choice" :: Text), "weights" .= Map.singleton ("z" :: Text) (2 :: Double)]]+ eitherDecode (encode settings) @?= Right settings,+ testCase "valid settings preserve sparse weights and endpoint thresholds" $ do+ mapM_ (\t -> validateSetting noul (NoulSetting t) @?= Right (NoulSetting t)) [0, 0.5, 1]+ validateSetting choice (ChoiceSetting (Map.singleton "z" 0)) @?= Right (ChoiceSetting (Map.singleton "z" 0)),+ testCase "validation names the field and rejects each broken rule" $ do+ let invalid =+ [ (noul, ScoreSetting [], "kind"),+ (noul, NoulSetting (-0.1), "threshold"),+ (noul, NoulSetting 1.1, "threshold"),+ (noul, NoulSetting (0 / 0), "finite"),+ (score, ScoreSetting [0.5], "N-1"),+ (score, ScoreSetting [0.5, 0.5], "increasing"),+ (score, ScoreSetting [1.5, 0.5], "increasing"),+ (score, ScoreSetting [0, 1], "(0,N-1)"),+ (score, ScoreSetting [1, 2], "(0,N-1)"),+ (score, ScoreSetting [0.5, 1 / 0], "finite"),+ (choice, ChoiceSetting (Map.singleton "unknown" 1), "declared"),+ (choice, ChoiceSetting (Map.singleton "z" (-1)), "non-negative"),+ (choice, ChoiceSetting (Map.singleton "a" (1 / 0)), "finite")+ ]+ mapM_+ ( \(j, setting, rule) -> case validateSetting j setting of+ Left msg -> do assertBool "field name" (judgmentField j `T.isInfixOf` msg); assertBool (T.unpack msg) (rule `T.isInfixOf` msg)+ Right s -> assertFailure ("accepted invalid setting " <> show s)+ )+ invalid,+ testProperty "score value is independent of valid cuts and level is monotone in value" $+ forAll (choose (0.01, 0.99)) $ \c ->+ forAll (choose (0, 1)) $ \p ->+ forAll (choose (p, 1)) $ \q ->+ let lo = ScoreEvidence (V.fromList [1 - p, p]) Nothing+ hi = ScoreEvidence (V.fromList [1 - q, q]) Nothing+ in case (deriveScore [c] lo, deriveScore [0.5] lo, deriveScore [c] hi) of+ (Right (v, l), Right (v', _), Right (w, m)) -> v == v' && v <= w && l <= m+ _ -> False+ ]
test/ErrorSpec.hs view
@@ -33,7 +33,8 @@ httpStatus = Just 400, retryAfterSeconds = Nothing, refusalCategory = Nothing,- exitCode = Nothing+ exitCode = Nothing,+ requestProblem = Nothing } @?= ContextWindowExceeded "context length exceeded", testCase "isTransient classification" $ do
test/Main.hs view
@@ -7,6 +7,7 @@ import CombinatorSpec qualified import ConstraintSpec qualified import ContinuationSpec qualified+import DecisionDeriveSpec qualified import EndToEndSpec qualified import ErrorSpec qualified import FallbackGuideSpec qualified@@ -38,6 +39,7 @@ testGroup "shikumi" [ ResponsesSpec.tests,+ DecisionDeriveSpec.tests, RequestDefaultsSpec.tests, ContinuationSpec.tests, ErrorSpec.tests,
test/ProgramAcceptanceSpec.hs view
@@ -32,6 +32,7 @@ ( Demo (..), Params (..), Program,+ emptyParams, foldParams, mapParamsAt, pipeline,@@ -104,4 +105,4 @@ where -- node 1 should remain the default overlay emptyParamsLike :: Params- emptyParamsLike = Params Nothing []+ emptyParamsLike = emptyParams
test/RequestDefaultsSpec.hs view
@@ -2,6 +2,7 @@ import Baikai (ToolChoice (..), emptyContext, emptyModel, emptyOptions, emptyResponse) import Baikai.Evidence qualified as E+import Baikai.Model (OutputKind (..)) import Baikai.Speed (Speed (..)) import Baikai.ThinkingLevel (ThinkingLevel (..)) import Control.Concurrent (forkFinally, newEmptyMVar, putMVar, takeMVar)@@ -26,7 +27,28 @@ tests = testGroup "Request defaults"- [ testCase "empty is identity and unrelated policy and metadata survive" $ do+ [ testCase "judgment-only calls fill evidence alone and keep explicit controls" $ do+ ref <- newIORef []+ result <- runEff+ . runErrorNoCallStack @ShikumiError+ . interpret+ ( \_ -> \case+ Complete _ _ o -> liftIO (modifyIORef' ref (++ [o])) >> pure emptyResponse+ Stream {} -> pure []+ )+ . withRequestDefaults defaults+ $ do+ _ <- complete (emptyModel & #outputKind .~ JudgmentOutput) emptyContext emptyOptions+ _ <- complete (emptyModel & #outputKind .~ JudgmentOutput) emptyContext (emptyOptions & #temperature .~ Just 0.2)+ pure ()+ result @?= Right ()+ captured <- readIORef ref+ map (^. #thinking) captured @?= [Nothing, Nothing]+ map (^. #speed) captured @?= [Nothing, Nothing]+ map (^. #maxTokens) captured @?= [Nothing, Nothing]+ map (^. #temperature) captured @?= [Nothing, Just 0.2]+ map (^. #evidence) captured @?= replicate 2 (defaultEvidence defaults),+ testCase "empty is identity and unrelated policy and metadata survive" $ do let opts = emptyOptions & #temperature .~ Just 0.7
test/RoutingSpec.hs view
@@ -55,6 +55,7 @@ Params (..), Program (Predict), TempSchedule (..),+ emptyParams, runProgram, runProgramConc, )@@ -220,7 +221,7 @@ Demo (object ["subject" .= ("dogs" :: Text)]) (object ["points" .= (["one", "two"] :: [Text])])- node = Predict topicToOutline (Params Nothing [demo])+ node = Predict topicToOutline (emptyParams {demos = [demo]}) captured <- captureRouted openai_gpt_4o_mini node (Topic "cats") case captured of [(_, ctx, _)] -> case assistantTexts ctx of
test/SerializeSpec.hs view
@@ -35,10 +35,10 @@ $ defaultProg node0Params :: Params-node0Params = Params (Just "outline carefully") [Demo (String "a topic") (String "an outline")]+node0Params = emptyParams {instructionOverride = Just "outline carefully", demos = [Demo (String "a topic") (String "an outline")]} node1Params :: Params-node1Params = Params (Just "draft fluently") []+node1Params = emptyParams {instructionOverride = Just "draft fluently"} tests :: TestTree tests =
+ test/compile-fail/FreeFormDecision.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-}++module FreeFormDecision where++import Data.Text (Text)+import GHC.Generics (Generic)+import Shikumi.Decision (DecisionOutputs)+import Shikumi.Schema (Field)++newtype InvalidDecision = InvalidDecision {summary :: Field "Explain the answer" Text}+ deriving stock (Generic)+ deriving anyclass (DecisionOutputs)
+ test/compile-fail/TooFewScoreLevels.hs view
@@ -0,0 +1,8 @@+{-# LANGUAGE DataKinds #-}++module TooFewScoreLevels where++import Shikumi.Decision (Score (..))++invalidValue :: Score '["Only"]+invalidValue = Score 0 0 mempty Nothing
+ test/compile-fail/TooManyScoreLevels.hs view
@@ -0,0 +1,8 @@+{-# LANGUAGE DataKinds #-}++module TooManyScoreLevels where++import Shikumi.Decision (Score (..))++invalidValue :: Score '["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]+invalidValue = Score 0 0 mempty Nothing