Documentation
¶
Overview ¶
Package exp serves decision models, the models that speak the System One protocol, and holds the question types a decision is declared with. A decision model does not generate text. It evaluates a state against typed questions and returns one typed answer per question, with calibrated probabilities.
The plugin serves such a model as one that speaks only constrained JSON, which is the subset of the generate API it fits exactly. The questions are the fields of the output type, declared with Choice, Score, Noul, and NoulOf, so a decision is one typed generate call with nothing more than the model and the state:
type Dept string
func (Dept) Criteria() map[Dept]string {
return map[Dept]string{
"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations",
"other": "None of the above",
}
}
type Triage struct {
Department systemonex.Choice[Dept] `json:"department" jsonschema_description:"Which team should handle this?"`
IsUrgent systemonex.Noul `json:"is_urgent" jsonschema_description:"Does the ticket explicitly communicate time pressure?"`
}
g := genkit.Init(ctx, genkit.WithPlugins(systemonex.TypeSafe()))
out, resp, err := genkit.GenerateData[Triage](ctx, g,
ai.WithModelName("typesafe/jev-1.13.0"),
ai.WithPrompt(ticket))
if out.Department.Confidence < 0.6 {
// route to a human
}
Criteria and levels are strings on their types. GuidedOption, GuidedRubric, and GuidedYesNo add the structured form the wire format also takes, such as examples per option. Questions whose options or instructions come from data are built at run time with Schema and answered as a map of Answer. ResponseInfo reads the model version that answered and the raw answers off a response.
The types are the same for every model that speaks the protocol, so one decision type works with each of them. SystemOne is the plugin that serves the models, from any server that speaks the protocol; TypeSafe and the other constructors set it up for the known ones.
This package is a preview: its API may change in any minor release.
Index ¶
- func Schema(questions map[string]Question) map[string]any
- type Answer
- type Choice
- type ChoiceOption
- type ChoiceQuestion
- type Config
- type GuidedOption
- type GuidedRubric
- type GuidedYesNo
- type Info
- type MediaSupport
- type ModelSpec
- type NoCriteria
- type Noul
- type NoulOf
- type NoulQuestion
- type Option
- type Question
- type Rubric
- type Score
- type ScoreQuestion
- type SystemOne
- func (s *SystemOne) Init(ctx context.Context) []api.Action
- func (s *SystemOne) ListActions(ctx context.Context) []api.ActionDesc
- func (s *SystemOne) ModelRef(id string, config *Config) ai.ModelRef
- func (s *SystemOne) Name() string
- func (s *SystemOne) ResolveAction(atype api.ActionType, id string) api.Action
- type YesNo
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Schema ¶
Schema is the output schema that asks the given questions, each answered under its key. Pass it with ai.WithOutputSchema and read the answers as a map of Answer:
options := make([]systemonex.ChoiceOption, 0, len(tools))
for _, tool := range tools {
options = append(options, systemonex.ChoiceOption{Name: tool.Name, Criteria: tool.Description})
}
answers, _, err := genkit.GenerateData[map[string]systemonex.Answer](ctx, g,
ai.WithModelName("typesafe/jev-1.13.0"),
ai.WithOutputSchema(systemonex.Schema(map[string]systemonex.Question{
"tool": systemonex.ChoiceQuestion{Instructions: "Which tool serves the request?", Options: options},
})),
ai.WithPrompt(request))
if err != nil {
return err
}
tool := (*answers)["tool"].Choice
Types ¶
type Answer ¶
type Answer struct {
Type string `json:"type,omitempty"`
Choice string `json:"choice,omitempty"`
Score float64 `json:"score,omitzero"`
Probability float64 `json:"noul,omitzero"`
Probabilities map[string]float64 `json:"probabilities,omitempty"`
Confidence float64 `json:"confidence,omitzero"`
Legend map[string]string `json:"legend,omitempty"`
}
Answer is the answer to a question of a Schema, with the fields of its kind set. Type names the kind: "choice" sets Choice, Probabilities over the options, and Confidence; "score" sets Score, Probabilities keyed by level number, Confidence, and Legend; "noul" sets Probability alone. They mean what the same fields of Choice, Score, and NoulOf mean. Decoding an answer sets Type from the field that names the kind, so a response's answers need no discriminator.
func (Answer) MarshalJSON ¶
MarshalJSON implements json.Marshaler. It writes the fields of the answer's kind, zeros included, so a score of 0 or a flat distribution's confidence of 0 survives a round trip, and none of another kind's. An answer with no Type writes its non-zero fields.
func (*Answer) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler. A missing type is read from the field every answer of a kind carries: choice, score, or noul.
type Choice ¶
type Choice[T Option[T]] struct { Choice T `json:"choice"` Probabilities map[T]float64 `json:"probabilities,omitempty"` Confidence float64 `json:"confidence"` }
Choice is the answer to a choice question: one option from T's criteria, the probability of every option, and how concentrated that distribution is. Its JSON is the wire answer, so an output type built from these fields is filled straight from the response.
Probabilities is the model's distribution over the options and sums to 1; Choice is the option with the most of it; Confidence, from 0 to 1, is how concentrated the distribution is.
func (Choice[T]) JSONSchema ¶
func (Choice[T]) JSONSchema() *jsonschema.Schema
JSONSchema encodes the question: the options and their criteria as a oneOf of constants, marked as a choice question. The options go out sorted by name, since a map has no order of its own.
type ChoiceOption ¶
ChoiceOption is one option of a ChoiceQuestion: the name that is the answer when it is chosen, what it means, and optionally the structured form of that meaning, as GuidedOption gives it for a Choice. An option with neither criteria nor guidance is described by its name.
type ChoiceQuestion ¶
type ChoiceQuestion struct {
// Instructions is the question: a string, or any JSON value, such as
// an object that names the field of the state the question is about.
Instructions any
Options []ChoiceOption
}
ChoiceQuestion asks for one of its options, which go out in the order given.
type Config ¶
Config is the per-request configuration of a decision model: StateJSON parses each text part of the state as JSON, so a prompt template that renders a JSON document produces an object state, and Extra merges fields into the request body that this package does not model, such as a gateway's session_id or trace.
type GuidedOption ¶
GuidedOption is an optional companion to Option for the structured form of a description the wire format takes: any JSON value, most often an object with labeled parts, such as what, not_for, and examples for an option. The strings of Criteria stay required, and the schema and the answers show them; guidance replaces a string on the wire only.
A value that encodes as a JSON object with no "what" key gets the string as its "what", so guidance adds to the description rather than replacing it. Any other value is sent as it is. An option that is absent, or whose value is nil, keeps its string.
func (Dept) Guidance() map[Dept]any {
return map[Dept]any{
"billing": map[string]any{
"not_for": "Progress of a refund already issued",
"examples": []string{"I was charged twice for one order."},
},
}
}
type GuidedRubric ¶
GuidedRubric is the GuidedOption of a Rubric: guidance keyed by level index, lowest level 0, such as summary and signals for a level. Score.Legend and Score.Label keep the strings of Levels.
type GuidedYesNo ¶
type GuidedYesNo interface {
Guidance() (yes, no any)
}
GuidedYesNo is the GuidedOption of a YesNo: guidance for the yes side and the no side, such as what and examples. The pair rule holds across both: each side needs a string or guidance, or neither does.
type Info ¶
Info is what a decision model's response carries beside the answers in the message: the model version that answered, which is what confidence thresholds are tuned against, a gateway's provider and generation ID, and the answers as the API sent them, guidance echoes and fields the answer types do not declare included. ResponseInfo reads it.
func ResponseInfo ¶
func ResponseInfo(resp *ai.ModelResponse) Info
ResponseInfo reads the Info of a decision model's response, from the model or from JSON that carried one, such as a flow's output or a stored trace. It is the zero Info for a response from any other model.
type MediaSupport ¶
type MediaSupport int
MediaSupport says whether models read one kind of media.
const ( // MediaDefault keeps the setting it overrides: for a model, the // plugin's; for the plugin, refusing the media. MediaDefault MediaSupport = iota // MediaSupported sends the media. MediaSupported // MediaUnsupported refuses a request with the media before it is sent. MediaUnsupported )
type ModelSpec ¶
type ModelSpec struct {
// Label names the model in the Dev UI. Empty means one built from the
// ID.
Label string
// Images, Audio, and Video override the plugin's for this model. The
// zero value keeps the plugin's, so an entry that only sets a Label
// changes nothing else.
Images, Audio, Video MediaSupport
}
ModelSpec describes one decision model.
type NoCriteria ¶
type NoCriteria struct{}
NoCriteria is the YesNo of a plain Noul: the question is its description alone.
func (NoCriteria) Criteria ¶
func (NoCriteria) Criteria() (yes, no string)
Criteria implements YesNo with no criteria.
type Noul ¶
type Noul = NoulOf[NoCriteria]
Noul is a NoulOf with no criteria: the question is its description alone, which is the usual yes/no question.
type NoulOf ¶
NoulOf is the answer to a yes/no question whose criteria come from C: the probability that the statement is true. A value near 0.5 means the model could not tell, not that the answer is "somewhat". There is no separate confidence; the probability is the whole answer.
func (NoulOf[C]) JSONSchema ¶
func (NoulOf[C]) JSONSchema() *jsonschema.Schema
JSONSchema encodes the question, marked as a noul question, with C's criteria on the x-true and x-false keywords when it has any.
type NoulQuestion ¶
type NoulQuestion struct {
// Instructions is the question: a string, or any JSON value.
Instructions any
Yes, No string
YesGuidance, NoGuidance any
}
NoulQuestion asks whether a statement is true, with what yes and no mean as YesNo and GuidedYesNo give them for a NoulOf: each side needs a string or guidance, or neither does.
type Option ¶
Option is the key type of a Choice: a string type that lists its own options and what each one means. The criteria are what the model chooses among, so they belong to the type rather than to any one call. A description is a string; GuidedOption adds the structured form the wire format also takes. Options known only at run time are asked with a ChoiceQuestion instead.
type Dept string
func (Dept) Criteria() map[Dept]string {
return map[Dept]string{
"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations",
"other": "None of the above",
}
}
type Question ¶
type Question interface {
// contains filtered or unexported methods
}
Question is one question of a set built at run time, for questions whose options, levels, or instructions come from data: the tools or products on hand, the nodes of a taxonomy, a tenant's own categories. ChoiceQuestion, ScoreQuestion, and NoulQuestion implement it, and Schema turns a set of them into an output schema. A question known when the code is written reads better as a field of an output type.
type Rubric ¶
type Rubric interface {
Levels() []string
}
Rubric supplies the ordered levels of a Score, lowest first. The level's index is its number on the wire, so a three-level rubric scores 0 to 2.
type Score ¶
type Score[L Rubric] struct { Score float64 `json:"score"` Probabilities map[string]float64 `json:"probabilities,omitempty"` Confidence float64 `json:"confidence"` Legend map[string]string `json:"legend,omitempty"` }
Score is the answer to a rubric question: the expected level, computed from the probability of each level, so it falls between levels when the model is split. Probabilities is that distribution, keyed by level number; Confidence, from 0 to 1, is how concentrated it is; Legend maps each level number back to its description, the string from L even when the level was sent with guidance.
func (Score[L]) JSONSchema ¶
func (Score[L]) JSONSchema() *jsonschema.Schema
JSONSchema encodes the question: the level descriptions ride on the x-levels keyword, since a fractional score cannot be a oneOf of levels.
func (Score[L]) Label ¶
Label is the rubric's description of Score.Level. It comes from L, so it does not depend on the answer carrying a legend.
type ScoreQuestion ¶
type ScoreQuestion struct {
// Instructions is the question: a string, or any JSON value.
Instructions any
Levels []string
Guidance map[int]any
}
ScoreQuestion rates the state on its levels, lowest first, as a Rubric gives them, with guidance keyed by level index as GuidedRubric gives it.
type SystemOne ¶
type SystemOne struct {
// Provider names the plugin and prefixes its model names. It is
// required, and must differ from every other plugin's name, so a host
// that serves chat models through another plugin needs a name of its
// own here, such as openrouter-decisions.
Provider string
// BaseURL is the server's root, which Path and ModelsPath are joined
// to. It is required, unless a constructor gives a default.
BaseURL string
// Path is the request path. Empty means /v1/systemone, and "/" means
// BaseURL itself, for a BaseURL that is the whole endpoint.
Path string
// ModelsPath is the path of the server's model listing. When empty, no
// listing is asked for, and the Dev UI shows the models in Models.
ModelsPath string
// APIKey is sent as a bearer token. A constructor reads it from the
// environment variable it names when this is empty; a server that takes
// no key, such as a local one, is reached with none.
APIKey string
// Models describes the models known ahead, keyed by the ID the server
// uses or by the full model name, Provider/ID; a server ID that itself
// starts with Provider/ takes the full name. They are listed in the Dev
// UI whether or not the server has a listing. Any other ID resolves
// too, as a model with no description.
Models map[string]ModelSpec
// Images, Audio, and Video say whether the models read each kind of
// media. The zero value refuses a request with media of that kind
// before it is sent, since media is an extension to the protocol, and
// a server that does not know a field can drop it and answer without
// the media, as OpenRouter's does for a model that reads none.
// [MediaSupported] sends it, for a server that takes it, and leaves a
// model that cannot read it to the server to refuse. A model's
// ModelSpec overrides each.
Images, Audio, Video MediaSupport
// HTTPClient sends the requests. It is the escape hatch to transport
// settings: timeouts, proxies, and client middleware. When nil, a
// client with a 30-second timeout is used.
HTTPClient *http.Client
// Headers are sent on every request, after the authorization header.
Headers http.Header
// Route, for a server whose wire is not the native one, builds the
// request it takes. It gets the model ID and the native body, which it
// may change, and returns the path to append to Path, which is joined
// with a slash when it does not start with one, and the body to send.
// When nil, the native body goes to Path.
Route func(model string, body map[string]any) (path string, out any, err error)
// Unwrap, for a server that wraps its responses, returns the native
// response from the body the server sent, such as by taking it out of
// an envelope. An error it returns is reported under Provider, as
// UNKNOWN unless it carries a Genkit status. When nil, the body is read
// as the native response.
Unwrap func(body []byte) ([]byte, error)
// contains filtered or unexported fields
}
SystemOne is a plugin for any server that speaks System One. It serves the server's decision models under Provider, by the IDs the server uses: a model the server calls d1 is Provider/d1, and one a gateway calls liquid/d1 is Provider/liquid/d1. Any ID resolves, so a model the server adds later works without a change here.
For a server with no constructor of its own, set the fields:
&systemonex.SystemOne{
Provider: "liquid",
BaseURL: "https://api.liquid.ai/decisions",
ModelsPath: "/v1/models",
APIKey: os.Getenv("LIQUID_API_KEY"),
Images: systemonex.MediaSupported,
}
The constructors, such as TypeSafe, return one set up for a known server; its fields can still be changed before the plugin is used. The fields are read once, by Init: a change after it has no effect.
func Ollama ¶
func Ollama() *SystemOne
Ollama is a local Ollama server, which serves the decision models it has pulled, such as Clef, and takes no key. Its models are ollama-decisions/<name> by Ollama's name, such as ollama-decisions/clef. The name is not ollama, which the plugin for Ollama's chat models has, so both serve one app. The server is http://localhost:11434 unless BaseURL says otherwise. The Dev UI lists Clef, and Models adds the other models pulled.
Ollama takes images as raw base64 rather than data URLs, which this server converts, and requires text in the state, so a request with no text, such as one of images alone, is refused before it is sent.
func OpenRouter ¶
func OpenRouter() *SystemOne
OpenRouter is OpenRouter's Decisions API, which serves several vendors' decision models under one key. Its models are openrouter-decisions/<ID> by OpenRouter's ID, such as openrouter-decisions/liquid/d1 and openrouter-decisions/typesafe/jev-1.13. The name is not openrouter, which the plugin for OpenRouter's chat models has, so both serve one app. The API key comes from OPENROUTER_API_KEY. The Dev UI lists the models OpenRouter lists as decision models, and the best known of them when the listing fails, and a request's cost is reported in the response's usage.
Its models are text only here. OpenRouter's Decisions API takes images in the state, not in the images field, and it can route a request to a model or a provider that drops them and answers without them, so a request with media is refused before it is sent.
func TypeSafe ¶
func TypeSafe() *SystemOne
TypeSafe is TypeSafe AI's own API, which serves jev, the first System One model. Its models are typesafe/<id>, such as typesafe/jev-1.13.0, and read text and JSON only. The API key comes from TYPESAFE_API_KEY and the base URL from TYPESAFE_BASE_URL, the variables TypeSafe's SDKs read. Pin a version in production: confidence thresholds tuned against one release do not carry over to the next.
func WorkersAI ¶
WorkersAI is Cloudflare Workers AI for one account. It serves Cloudflare's own decision models, Clef and Clef-flash, which read images, and Clef-omni, which reads images, audio, and video, and partners' models such as jev. Its models are cloudflare-decisions/<ID> by the Workers AI ID: cloudflare-decisions/@cf/cloudflare/clef, cloudflare-decisions/@cf/cloudflare/clef-flash, cloudflare-decisions/@cf/cloudflare/clef-omni, and cloudflare-decisions/typesafe/jev. The API token comes from CLOUDFLARE_API_TOKEN, and the account ID from CLOUDFLARE_ACCOUNT_ID when accountID is empty. A BaseURL with no {account} segment, such as a proxy's, needs no account ID.
Workers AI names one model per release, with no versions, so a threshold tuned against a model can move when Cloudflare updates it.
func (*SystemOne) Init ¶
Init implements api.Plugin. It builds the client, and panics without a provider name or a base URL, or without a key or an account ID for a server that requires one, since every request would fail, and when Models names one model twice. No actions are registered up front: models resolve by name.
func (*SystemOne) ListActions ¶
func (s *SystemOne) ListActions(ctx context.Context) []api.ActionDesc
ListActions implements api.DynamicPlugin. It lists the models in Models together with the ones the server's listing names. A listing that fails or takes too long leaves the models in Models, so the Dev UI still shows them while offline. A listing is kept for a few minutes, and a failed one is not tried again for a minute, so the Dev UI does not wait on the server each time it lists actions.
func (*SystemOne) ModelRef ¶
ModelRef returns a reference to one of the plugin's models with a config, for the places that take a reference rather than a name, such as a fallback list. The ID is the server's or the full model name, as a key of Models is: ModelRef("jev-1.13.0", cfg) and ModelRef("typesafe/jev-1.13.0", cfg) on TypeSafe are both typesafe/jev-1.13.0. With no config to attach, ai.WithModelName with the full name is the usual way to pick the model.
func (*SystemOne) ResolveAction ¶
ResolveAction implements api.DynamicPlugin. Models are the only action type served. The ID is forwarded as given; whether it exists is for the server to say when the first request is made.
type YesNo ¶
type YesNo interface {
Criteria() (yes, no string)
}
YesNo supplies what yes and no mean for a NoulOf question, as the criteria of a Choice and the levels of a Score come from their types. Both sides are given: the API takes the pair or nothing.
type Urgent struct{}
func (Urgent) Criteria() (yes, no string) {
return "Names a deadline, or says now or today", "No time pressure is expressed"
}