explainer

package
v0.13.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: GPL-3.0 Imports: 33 Imported by: 0

Documentation

Index

Constants

View Source
const InferredRacksShown = 5

InferredRacksShown is how many of the most likely leaves belong in a prompt. The tail is a long list of near-identical racks and says nothing the top few don't. Exported because the caller is what asks the rangefinder for them.

View Source
const (

	// ResponseBanner introduces what the model sent back.
	ResponseBanner = bannerRule + " MODEL RESPONSE " + bannerRule
)

Banners dividing a dump into sections. Each one labels everything below it up to the next banner, which is the only workable rule: the prompt is markdown, so a per-line marker can't distinguish our commentary from the content - "## What to say" is a heading in system.md and really is sent.

The eight-equals form is deliberate. A markdown setext heading is a line of nothing but "=", so a banner with text in it can't be mistaken for one in the model's reply.

Variables

This section is empty.

Functions

func ConceptIDs added in v0.13.6

func ConceptIDs(cs []*Concept) []string

ConceptIDs names the cards, for tests and for logging which ones a position pulled in.

func DottedPlay added in v0.13.6

func DottedPlay(playString string) string

DottedPlay converts a play from the playthrough notation the tables use, 5D (S)PIC(A), into the dotted form the move parser wants, 5D .PIC. - the parenthesized tiles are the ones already on the board. Exchanges and passes come back unchanged apart from their wrapping parentheses. Input that is already dotted is left alone.

func ToolSchemas added in v0.13.6

func ToolSchemas(tools []interfaces.Tool) string

ToolSchemas renders the tool definitions as JSON Schema, the way the OpenAI-compatible providers serialize them. Gemini builds an equivalent genai.Schema out of the same three pieces - name, description, parameters - so this is the shape of what every provider sends, not the exact bytes of any one of them.

Types

type Analyzer

type Analyzer struct {
	// contains filtered or unexported fields
}

Analyzer holds the game and the facts we computed about the position, and answers the tools' questions from them. Nothing here parses the prompt.

func NewAnalyzer

func NewAnalyzer() *Analyzer

func (*Analyzer) BuildFacts added in v0.13.6

func (a *Analyzer) BuildFacts(sim *montecarlo.Simmer, ss *stats.SimStats,
	req *ComparisonRequest, inf *InferenceInput) (*PositionFacts, error)

BuildFacts assembles the fact pack for the current position from a finished simulation, and keeps it as what the tools answer from. It must be called after the sim has stopped. A non-nil req asks for a head-to-head against that play; the simulation must already have evaluated it, which is what Simmer.AvoidPruningMoves is for. A non-nil inf adds what a read on the opponent's rack changed, if anything.

func (*Analyzer) EvaluateLeave

func (a *Analyzer) EvaluateLeave(leave string) (float64, error)

EvaluateLeave evaluates the value of a leave

func (*Analyzer) Facts added in v0.13.6

func (a *Analyzer) Facts() *PositionFacts

Facts returns the fact pack built for the current position, if any.

func (*Analyzer) GetFuturePlayMetadata

func (a *Analyzer) GetFuturePlayMetadata(playString string) (*FuturePlayMetadata, error)

GetFuturePlayMetadata analyzes a potential future play. If the play can be made in more than one way it returns the most likely one; use LookupFuturePlay to see every way.

func (*Analyzer) GetPlayMetadata

func (a *Analyzer) GetPlayMetadata(playString string) (*PlayMetadata, error)

GetPlayMetadata analyzes a play and returns metadata

func (*Analyzer) LookupFuturePlay added in v0.13.6

func (a *Analyzer) LookupFuturePlay(playString string) (*FuturePlayLookup, error)

LookupFuturePlay looks a follow-up play up in the facts computed for this position. It returns a *PlayNotFoundError if the play isn't one the simulation sampled.

func (*Analyzer) SetConfig

func (a *Analyzer) SetConfig(cfg *config.Config)

SetConfig sets the configuration for the analyzer

func (*Analyzer) SetGame added in v0.13.6

func (a *Analyzer) SetGame(tp *bot.BotTurnPlayer)

SetGame sets the position under analysis.

type Comparison added in v0.13.6

type Comparison struct {
	// Play is what we were asked to contrast the best play with.
	Play string `json:"play"`
	// FromHistory is true when it is the move the player actually made, as
	// opposed to one they named.
	FromHistory bool `json:"from_history"`
	// WasBest is true when the play we were asked about *is* the top play. It
	// then has nothing to be contrasted with, so Rival below is the runner-up
	// instead and there is still something to learn.
	WasBest bool `json:"was_best"`

	// Rival is the candidate actually contrasted against.
	Rival          *montecarlo.CandidateStats `json:"rival"`
	RivalPlayStats *stats.PlayStats           `json:"-"`
	RivalFollowups []*FollowupFact            `json:"rival_followups"`
	// RivalChances is RivalFollowups gathered into opportunities.
	RivalChances []*FollowupCluster `json:"rival_chances"`
	// TypicalNextScore is what our next turn averages after the rival, and
	// ChanceBaseline is what it is worth when none of the rival's chances come
	// up. See judgeChances.
	TypicalNextScore float64 `json:"typical_next_score"`
	ChanceBaseline   float64 `json:"chance_baseline"`

	Deltas Deltas `json:"deltas"`

	// OnlyBest and OnlyRival are the worthwhile follow-up plays each side has
	// that the other doesn't - the concrete "what you gave up" list.
	OnlyBest  []*FollowupFact `json:"only_best"`
	OnlyRival []*FollowupFact `json:"only_rival"`
}

Comparison answers "why is this better than the move I made?". The play in question is normally the one the player actually made, taken from the game history; it can also be named outright.

type ComparisonRequest added in v0.13.6

type ComparisonRequest struct {
	Move *move.Move
	// FromHistory distinguishes "the move you actually made", which we found
	// ourselves, from one the user named.
	FromHistory bool
}

ComparisonRequest names a play the best one should be contrasted with.

type Concept added in v0.13.6

type Concept struct {
	// ID is the card's name, taken from its frontmatter.
	ID string
	// Priority orders the cards in the prompt, lowest first.
	Priority int
	// When lists flag names. The card ships if any of them is set, so
	// "always" is just a flag that is always true.
	When []string
	Body string
}

Concept is one card: a piece of Scrabble knowledge plus the conditions under which it is worth spending prompt on.

func SelectConcepts added in v0.13.6

func SelectConcepts(fl Flags) ([]*Concept, error)

SelectConcepts returns the cards this position calls for, in prompt order.

type Config

type Config struct {
	Provider  string // "gemini", "openai", "openrouter", or "deepseek"
	APIKey    string
	Model     string
	BaseURL   string // optional: override base URL (e.g. for LM Studio at http://127.0.0.1:1234/v1)
	UseQuirky bool
}

Config holds configuration for the explainer service

func DefaultConfig

func DefaultConfig(macondoConfig *macondo.Config) *Config

DefaultConfig returns a default configuration from macondo config

type Deltas added in v0.13.6

type Deltas struct {
	WinPct     float64 `json:"win_pct"`
	Equity     float64 `json:"equity"`
	Score      int     `json:"score"`
	LeaveValue float64 `json:"leave_value"`
	// The opponent's reply, one ply out: a negative OppMeanScore means the
	// best play holds them to less.
	OppMeanScore float64 `json:"opp_mean_score"`
	OppStdev     float64 `json:"opp_stdev"`
	OppBingoPct  float64 `json:"opp_bingo_pct"`
	// Our own next turn, two plies out.
	OurMeanScore float64 `json:"our_mean_score"`
	OurBingoPct  float64 `json:"our_bingo_pct"`
	// BestUpside and RivalUpside are the expected points each play's big
	// follow-up chances add to its next turn, over a next turn without them.
	// The gap between them is often the whole reason one play wins, and it is
	// invisible in every other figure here.
	BestUpside  float64 `json:"best_upside"`
	RivalUpside float64 `json:"rival_upside"`
	// ChancesShare and OrdinaryShare split OurMeanScore into the part that is
	// the two plays' big chances and the part that is their ordinary turns.
	// They add to OurMeanScore, which is the whole point of them: without the
	// split, the mean difference and the upside difference read as two
	// separate arguments for the same play when they are one argument counted
	// twice. SplitKnown is false when a play has no second ply to split.
	ChancesShare  float64 `json:"chances_share"`
	OrdinaryShare float64 `json:"ordinary_share"`
	SplitKnown    bool    `json:"split_known"`
	// Established is true when the two win% confidence intervals don't
	// overlap. When it is false the simulation has not actually shown one
	// play to be better, and saying so would be overclaiming.
	Established bool `json:"established"`
}

Deltas is the head-to-head arithmetic between the best play and the one it is being contrasted with. Every figure is best minus rival, so a positive number is the best play's advantage.

type EvaluateLeaveTool

type EvaluateLeaveTool struct {
	// contains filtered or unexported fields
}

EvaluateLeaveTool evaluates the value of a leave

func NewEvaluateLeaveTool

func NewEvaluateLeaveTool(analyzer *Analyzer) *EvaluateLeaveTool

func (*EvaluateLeaveTool) Description

func (t *EvaluateLeaveTool) Description() string

func (*EvaluateLeaveTool) Execute

func (t *EvaluateLeaveTool) Execute(ctx context.Context, args string) (string, error)

func (*EvaluateLeaveTool) Name

func (t *EvaluateLeaveTool) Name() string

func (*EvaluateLeaveTool) Parameters

func (t *EvaluateLeaveTool) Parameters() map[string]interfaces.ParameterSpec

func (*EvaluateLeaveTool) Run

func (t *EvaluateLeaveTool) Run(ctx context.Context, args string) (string, error)

type Exchange added in v0.13.6

type Exchange struct {
	Prompt   *Prompt
	Response string
	// BestPlay and Comparison say what the explanation was about, so a dump
	// after the fact is identifiable.
	BestPlay   string
	Comparison string
}

Exchange is what was last sent to the model and what came back. The prompt is assembled from so many computed pieces that being able to read the exact thing that produced an explanation is the only practical way to debug one.

func (*Exchange) String added in v0.13.6

func (e *Exchange) String() string

String renders the exchange for a human to read: our notes, then the two messages exactly as sent, then the reply. Each section is introduced by a banner and runs to the next one.

type ExplainInput added in v0.13.6

type ExplainInput struct {
	Game     *bot.BotTurnPlayer
	Simmer   *montecarlo.Simmer
	SimStats *stats.SimStats
	// Compare asks for a head-to-head against a particular play, usually the
	// one the player actually made. The simulation must already have
	// evaluated it - see Simmer.AvoidPruningMoves.
	Compare *ComparisonRequest
	// Inference is the read on the opponent's rack that Simmer was run with,
	// plus the same plays simmed without it. Nil when no read was taken.
	Inference *InferenceInput
	// Model overrides the configured model for this one explanation, so that
	// several can be tried on the same position without editing any config.
	// Empty means whatever the provider is configured to use.
	Model string
}

ExplainInput is the position to explain. The service builds everything it sends to the model from these three, so nobody has to pre-render tables and nobody has to parse them back.

type ExplainResult

type ExplainResult struct {
	Explanation  string
	InputTokens  int
	OutputTokens int
	// Provider and Model say who actually answered. Worth reporting because
	// both can now change between one explanation and the next.
	Provider string
	Model    string
	// Concepts names the concept cards this position pulled into the prompt.
	Concepts []string
	// Prompt is exactly what was sent. Also kept on the service, so it can be
	// read back after the fact.
	Prompt *Prompt
}

ExplainResult contains the explanation from the AI

type Flags added in v0.13.6

type Flags map[string]bool

Flags are the yes/no answers that decide which concept cards get sent. Every one of them replaces a paragraph that used to ship on every single call.

type FollowupCluster added in v0.13.6

type FollowupCluster struct {
	// Anchor is where the opportunity is: the tiles it runs through and the
	// lane they are in, like "(QUAD) in row 2", or a bare square when there is
	// nothing on the board to name it by.
	Anchor string `json:"anchor"`
	// Through is the letters already on the board that every play here runs
	// through, empty when the opportunity is an open square rather than a hook.
	Through string `json:"through"`
	// Plays are the ways of taking it, most likely first.
	Plays []*FollowupFact `json:"plays"`
	// Pct, MinScore and MaxScore cover the cluster as a whole.
	Pct      float64 `json:"pct"`
	AvgScore float64 `json:"avg_score"`
	MinScore int     `json:"min_score"`
	MaxScore int     `json:"max_score"`

	IsSetup     bool    `json:"is_setup"`
	IsBigChance bool    `json:"is_big_chance"`
	Upside      float64 `json:"upside"`
	// Baseline is what our next turn is worth when this chance does not come
	// up, which is what Upside is measured over. It is not the play's mean
	// next turn: a chance that comes up often is a large part of that mean,
	// and measuring it against a number it dominates hides it. See
	// chanceBaseline.
	Baseline float64 `json:"baseline"`
}

FollowupCluster is one opportunity, and every play that takes it. Plays that start on the same square and run through the same tiles already on the board are the same chance reached by different draws - 2F (QUAD)RUPLE, RUPLY, RUPED and RUPOLE are one hook and four ways of filling it.

The distinction is the whole reason this type exists. Judged one at a time those four are 4.91%, 4.81%, 3.49% and 2.58% - each individually too rare to build a turn around, and each rejected. Together they are 15.79% of the time for an average of 48.6 points, which is the single biggest thing about the position. A chance split across spellings is still one chance.

func (*FollowupCluster) Grouped added in v0.13.6

func (c *FollowupCluster) Grouped() bool

Grouped reports whether this is several plays rather than one.

func (*FollowupCluster) Label added in v0.13.6

func (c *FollowupCluster) Label() string

Label names the cluster. One play is named by itself. Several sharing a hook are named by the hook - "(QUAD) in row 2" says what the chance is. Several sharing only a square have no hook to name, and a bare "J9" says nothing at all, so those are named by their words: "J9 CAPERED / PEASCOD".

func (*FollowupCluster) NeededDraws added in v0.13.6

func (c *FollowupCluster) NeededDraws() []string

NeededDraws is every draw that reaches any play in the cluster, in the order the plays came in and without repeats.

func (*FollowupCluster) Requirement added in v0.13.6

func (c *FollowupCluster) Requirement() string

Requirement is what has to happen before the cluster's likeliest play is available.

func (*FollowupCluster) Worthwhile added in v0.13.6

func (c *FollowupCluster) Worthwhile() bool

Worthwhile reports whether the cluster is worth a sentence.

type FollowupFact added in v0.13.6

type FollowupFact struct {
	*stats.FollowupFamily
	// WayRequirements runs parallel to Ways. Each entry is "none", "requires
	// opponent play", or "requires us to play <best play> first". The last is
	// what actually makes a setup a setup: the follow-up doesn't exist unless
	// we make this play.
	WayRequirements []string `json:"way_requirements"`
	// IsSetup means our best play creates this opportunity.
	IsSetup bool `json:"is_setup"`
	// IsBigChance means the play is far bigger than an ordinary next turn and
	// comes up often enough for that to be worth playing for, whether or not
	// our play is what created it.
	IsBigChance bool `json:"is_big_chance"`
	// Upside is expected points above a next turn without this chance in it:
	// how often the play comes up times how much bigger it is than the turns
	// where it doesn't. See judgeChances for why the comparison is against
	// those turns rather than against the mean of all of them.
	Upside float64 `json:"upside"`
	// Baseline is what those turns are worth.
	Baseline float64 `json:"baseline"`
}

FollowupFact is one of our sampled follow-up plays, with the judgments the model used to have to make about it already made.

func (*FollowupFact) AvgScore added in v0.13.6

func (f *FollowupFact) AvgScore() float64

AvgScore is the score across the ways of making this play, weighted by how often each came up. A grouped play's MaxScore is only its luckiest route, so judging the play by it would overstate what it is worth.

func (*FollowupFact) Requirement added in v0.13.6

func (f *FollowupFact) Requirement() string

Requirement is what has to happen before the play's most likely route is available.

func (*FollowupFact) Worthwhile added in v0.13.6

func (f *FollowupFact) Worthwhile() bool

Worthwhile reports whether the follow-up is worth a sentence: either our play creates it, or it is big enough and likely enough to matter.

type FuturePlayFamily added in v0.13.6

type FuturePlayFamily struct {
	Play              string   `json:"play"`
	CombinedPercent   float64  `json:"combined_probability_percent"`
	ScoreRange        string   `json:"score_range"`
	NeededDrawOptions []string `json:"needed_draw_options"` // any one of these unlocks the play
	IsSetup           bool     `json:"is_setup"`
}

FuturePlayFamily describes a play that can be made in more than one way, because a blank can stand in for different tiles. The combined percentage is the one to quote: each individual way is only part of the chance of making the play.

type FuturePlayLookup added in v0.13.6

type FuturePlayLookup struct {
	Asked  string // the specific way the model named, if it named one
	Family *FuturePlayFamily
	Ways   []*FuturePlayMetadata
}

FuturePlayLookup is the result of looking a follow-up play up in the sim stats: every way of making it, plus the combined figures when there is more than one.

type FuturePlayMetadata

type FuturePlayMetadata struct {
	Play               string   `json:"play"`
	Score              int      `json:"score"`
	IsBingo            bool     `json:"is_bingo"`
	NeededDraw         []string `json:"needed_draw"`         // tiles needed from bag
	RequiresOtherPlay  string   `json:"requires_opp_play"`   // opponent play needed first
	ProbabilityPercent float64  `json:"probability_percent"` // likelihood of this play
	IsSetup            bool     `json:"is_setup"`
}

FuturePlayMetadata represents metadata about a potential future play

type GetOurFuturePlayMetadataTool

type GetOurFuturePlayMetadataTool struct {
	// contains filtered or unexported fields
}

GetOurFuturePlayMetadataTool analyzes metadata for a potential future play

func NewGetOurFuturePlayMetadataTool

func NewGetOurFuturePlayMetadataTool(analyzer *Analyzer) *GetOurFuturePlayMetadataTool

func (*GetOurFuturePlayMetadataTool) Description

func (t *GetOurFuturePlayMetadataTool) Description() string

func (*GetOurFuturePlayMetadataTool) Execute

func (*GetOurFuturePlayMetadataTool) Name

func (*GetOurFuturePlayMetadataTool) Parameters

func (*GetOurFuturePlayMetadataTool) Run

type GetOurPlayMetadataTool

type GetOurPlayMetadataTool struct {
	// contains filtered or unexported fields
}

GetOurPlayMetadataTool analyzes metadata for a current play

func NewGetOurPlayMetadataTool

func NewGetOurPlayMetadataTool(analyzer *Analyzer) *GetOurPlayMetadataTool

func (*GetOurPlayMetadataTool) Description

func (t *GetOurPlayMetadataTool) Description() string

func (*GetOurPlayMetadataTool) Execute

func (t *GetOurPlayMetadataTool) Execute(ctx context.Context, args string) (string, error)

func (*GetOurPlayMetadataTool) Name

func (t *GetOurPlayMetadataTool) Name() string

func (*GetOurPlayMetadataTool) Parameters

func (*GetOurPlayMetadataTool) Run

type InferenceFacts added in v0.13.6

type InferenceFacts struct {
	Summary *rangefinder.InferenceSummary `json:"summary"`
	// Outliers are the tiles the read actually says something about, ordered
	// by how far it moved them.
	Outliers []rangefinder.TileDeviation `json:"outliers"`
	// ShapeRead is set when the read is about the makeup of the rack rather
	// than about any one tile. It is a separate finding because it can be the
	// only one: a read can leave every letter within a tenth of a tile of
	// chance and still say, unmistakably, that they kept consonants.
	ShapeRead *rangefinder.RackShape `json:"shape_read,omitempty"`

	// Baseline is the same plays simmed without the read, so we can say what
	// the read changed rather than only what it concluded.
	Baseline []montecarlo.CandidateStats `json:"-"`
	// BaselineBest is the play we would have recommended knowing nothing.
	BaselineBest *montecarlo.CandidateStats `json:"baseline_best"`
	// BaselineOfBest is the recommended play's own showing in that sim, which
	// is what WinPctShift is measured against. Nil if it wasn't simmed there.
	BaselineOfBest *montecarlo.CandidateStats `json:"baseline_of_best"`

	// ChangedTopPlay is the strongest thing a read can do: recommend a
	// different play than we would have made without it.
	ChangedTopPlay bool `json:"changed_top_play"`
	// WinPctShift is the recommended play's win% with the read minus without.
	WinPctShift float64 `json:"win_pct_shift"`
	// Established is true when that shift is outside both sims' confidence
	// intervals. The two runs stop independently, so they aren't equally
	// sampled - non-overlapping intervals still means a real difference, but
	// the size of it shouldn't be read too precisely.
	Established bool `json:"established"`
	// Decisive is Established plus a practical significance test. A shift can
	// be statistically real and mean nothing: in a position already won, win
	// probabilities saturate, their intervals shrink to almost nothing, and a
	// swing from 97.9% to 97.0% clears the statistical bar while changing
	// nothing about how to play. Decisive is what gates the subject.
	Decisive bool `json:"decisive"`

	// Informative is true when the posterior deviates from chance enough to
	// be worth a sentence.
	Informative bool `json:"informative"`
	// Matters gates the whole subject. An uninformative read, or one that
	// moves nothing, is not mentioned at all.
	Matters bool `json:"matters"`
}

InferenceFacts is what the opponent's last play gave away, and whether it changed anything. Both halves matter: a read that says something startling about their rack but leaves the recommendation untouched is a curiosity, not a lesson, and neither half is worth prompt on its own.

type InferenceInput added in v0.13.6

type InferenceInput struct {
	Summary *rangefinder.InferenceSummary
	// Baseline is CandidateStats from a sim run with inference off, best
	// win% first.
	Baseline []montecarlo.CandidateStats
}

InferenceInput is a read on the opponent's rack, together with the same plays simmed without it. Both are needed: the read on its own can't say what it changed.

type LaneComparison added in v0.13.6

type LaneComparison struct {
	Play  string
	Best  bool
	Stats *stats.LaneStats
}

LaneComparison is where one candidate's sampled opponent replies land.

type Phase added in v0.13.6

type Phase string

Phase is where in the game we are. It decides which advice applies.

const (
	PhaseOpening Phase = "opening"
	PhaseMidgame Phase = "midgame"
	// PhaseLateMidgame is the stretch where the endgame is coming but the
	// position is not yet a pre-endgame. Naming it keeps the model from
	// reaching for pre-endgame language too early.
	PhaseLateMidgame Phase = "late midgame (endgame approaching)"
	PhasePreEndgame  Phase = "pre-endgame"
)

type PlayMetadata

type PlayMetadata struct {
	Play           string `json:"play"`
	Score          int    `json:"score"`
	TilesUsed      int    `json:"tiles_used"`
	IsBingo        bool   `json:"is_bingo"`
	VowelsInLeave  int    `json:"vowels_in_leave"`
	ConsonantsLeft int    `json:"consonants_in_leave"`
	LeaveBalance   string `json:"leave_balance"` // "balanced", "vowel-heavy", "consonant-heavy"
}

PlayMetadata represents metadata about a Scrabble play

type PlayNotFoundError added in v0.13.6

type PlayNotFoundError struct {
	Play      string
	Available []string
}

PlayNotFoundError is returned when a play is not one of the sampled follow-up plays. It carries the plays we do know about so that the tool can hand them back to the model instead of letting it guess at spellings.

func (*PlayNotFoundError) Error added in v0.13.6

func (e *PlayNotFoundError) Error() string

func (*PlayNotFoundError) ToolMessage added in v0.13.6

func (e *PlayNotFoundError) ToolMessage() string

ToolMessage is what we hand back to the LLM. It is deliberately not an error; a plain result that lists the valid plays lets the model correct itself in a single turn instead of retrying the same lookup.

type PositionFacts added in v0.13.6

type PositionFacts struct {
	Rack        string
	Lexicon     string
	BagCount    int
	UnseenCount int
	OppRackSize int
	Spread      int
	Phase       Phase

	UnseenVowels     int
	UnseenConsonants int
	UnseenBlanks     int
	UnseenPowerTiles []string

	Iterations int
	Candidates []montecarlo.CandidateStats
	Best       *montecarlo.CandidateStats

	PlayStats *stats.PlayStats
	Followups []*FollowupFact
	// Chances is Followups gathered into opportunities: the plays that take the
	// same hook are one chance, and are judged and reported as one. Ordered by
	// upside, biggest first.
	Chances []*FollowupCluster
	Lanes   []*LaneComparison
	// TypicalNextScore is what our next turn averages after the best play,
	// chances and all.
	TypicalNextScore float64
	// ChanceBaseline is what that turn is worth when none of the chances come
	// up, which is what every chance is measured against - see judgeChances.
	// The gap between the two is what the chances are worth.
	ChanceBaseline float64

	// BestByEquity is the candidate with the highest equity, which is not
	// always the one with the highest win%.
	BestByEquity *montecarlo.CandidateStats

	// Comparison is set when we were asked why the best play beats some other
	// play - usually the one the player actually made.
	Comparison *Comparison

	// Inference is set when the position was analyzed with a read on the
	// opponent's rack. It is nil unless `explain -infer` asked for one.
	Inference *InferenceFacts

	Flags Flags
}

PositionFacts is everything we know about the position, computed rather than described.

func (*PositionFacts) Ask added in v0.13.6

func (f *PositionFacts) Ask() string

Ask is the question the explanation has to answer, which is a different question when the reader has a play of their own on the table.

func (*PositionFacts) Render added in v0.13.6

func (f *PositionFacts) Render() string

Render writes the position, the candidates, and what the simulation saw happen next.

func (*PositionFacts) SetFollowups added in v0.13.6

func (f *PositionFacts) SetFollowups(fs []*FollowupFact)

SetFollowups records the sampled follow-ups and gathers them into the opportunities they take. The two are always set together - the flat list is the table, the clusters are what gets judged and reported - and going through here is what keeps them from disagreeing.

Best and TypicalNextScore have to be set first: a chance is only big or small relative to what an ordinary turn after this play is worth.

type Prompt added in v0.13.6

type Prompt struct {
	System string
	User   string
	// Concepts names the cards this position pulled in. Useful for logging
	// and for tests that assert a position asks for the right knowledge.
	Concepts []string
	// Tools are the function definitions sent alongside the messages. They
	// appear in neither message's text - the API carries them in a field of
	// their own - so a dump showing only the messages would be missing
	// instructions the model really does receive, and some of them are
	// substantive: get_our_future_play_metadata's description is what tells
	// the model it may only look up plays from the follow-up table.
	Tools []interfaces.Tool
}

Prompt is what gets sent: stable instructions in the system message, and everything that changes with the position in the user message. Splitting them this way is also what lets a provider cache the system half.

func BuildPrompt added in v0.13.6

func BuildPrompt(f *PositionFacts, quirky bool) (*Prompt, error)

BuildPrompt assembles the prompt for a position. Which concept cards it includes is decided by the facts, not by the model and not by a human guessing which ones might come up.

func (*Prompt) Notes added in v0.13.6

func (p *Prompt) Notes(headline string) string

Notes is Macondo's own commentary to print above a prompt dump: what the prompt was for, and which concept cards the position pulled in. It ends with a banner, so everything it says is bounded by position rather than by any per-line marker. headline, if given, says what the prompt was about.

func (*Prompt) String added in v0.13.6

func (p *Prompt) String() string

String is the whole prompt as one blob, for debugging and for the MACONDO_NO_LLM path. Sections run in the order the request carries them: tools, then the system message, then the user message.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service provides the main explainer service

func NewService

func NewService(macondoConfig *macondo.Config) *Service

NewService creates a new explainer service

func (*Service) Explain

func (s *Service) Explain(ctx context.Context, in *ExplainInput) (*ExplainResult, error)

Explain generates an explanation for a finished simulation.

func (*Service) LastExchange added in v0.13.6

func (s *Service) LastExchange() *Exchange

LastExchange returns the most recent prompt and response, or nil if this service hasn't explained anything yet.

Directories

Path Synopsis
Command example runs a real simulation on a fixed position and asks the configured LLM to explain the best play.
Command example runs a real simulation on a fixed position and asks the configured LLM to explain the best play.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL