systemone

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 19 Imported by: 0

README

systemone

CI Go Reference

A fully typed Go client for the TypeSafe System One API. Send one state and a set of questions; get one answer per question, each bound to the Go type you declared the question with, so options and score levels are checked by the compiler rather than compared against string literals.

go get github.com/justintout/systemone

No dependencies outside the standard library. Requires Go 1.24.

This is an unofficial, community-maintained client. It is not built, endorsed, or supported by TypeSafe. The official SDKs are Python and JavaScript.

Usage

A question is a value. Declare it once with the Go type its answer uses, then read the answer back through the same handle.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
)

type Team string

const (
	Billing   Team = "billing"
	Technical Team = "technical"
	Sales     Team = "sales"
)

type Anger int

const (
	Calm Anger = iota
	Frustrated
	Furious
)

var (
	team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
		systemone.Option(Billing, "Payments, invoicing, refunds"),
		systemone.Option(Technical, "Bugs, outages, integrations"),
		systemone.Option(Sales, "Pricing, upgrades, new accounts"),
	)
	urgent = systemone.NewNoul("urgent", "Does this convey urgency?",
		systemone.Yes("Explicitly time-sensitive"),
		systemone.No("No urgency expressed"),
	)
	anger = systemone.NewScore[Anger]("anger", "How frustrated is the customer?",
		systemone.Levels[Anger](
			"Calm and matter-of-fact",
			"Visibly frustrated",
			"Angry, threatening to leave",
		),
	)
)

func main() {
	client, err := systemone.New() // reads TYPESAFE_API_KEY
	if err != nil {
		log.Fatal(err)
	}

	ticket := map[string]any{
		"subject": "Payouts failing",
		"body":    "Help! My payouts have been failing for 3 days and nobody has replied.",
	}

	res, err := client.Ask(context.Background(), ticket, team, urgent, anger)
	if err != nil {
		log.Fatal(err)
	}

	t, err := team.From(res)   // systemone.ChoiceAnswer[Team]
	if err != nil {
		log.Fatal(err)
	}
	a, err := anger.From(res)  // systemone.ScoreAnswer[Anger]
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(t.Value, t.Confidence, a.Level, a.Describe())
}

From is the only way to reach an answer, which is what keeps the typing honest: there is no untyped map to fall back to. t.Value is a Team, so a switch over it is checked against the set the question declared. A typo in an option name, or a level enum borrowed from a different question, is a build failure rather than a branch that silently never fires.

Independent questions belong in one request. They are answered in parallel against the same state and cannot see one another's answers, which makes speculative questions cheap: ask the branch-specific ones up front and read only the answers your code ends up needing.

Primitives

Question Answer Use it for
NewChoice[T ~string] ChoiceAnswer[T]: Value, Probabilities, Confidence One of a defined set
NewNoul NoulAnswer: Value, the probability of yes Whether a condition holds
NewScore[T ~int] ScoreAnswer[T]: Value, Level, Legend, Probabilities, Confidence Degree along ordered levels

Instructions and every criterion take a string or any JSON-encodable structure, so a question can carry named data it refers to by backticked path:

var duplicate = systemone.NewNoul("duplicate", map[string]any{
	"candidate": map[string]string{"name": "John Smith", "last_employer": "Google"},
	"question":  "Is the resume for the same person as `candidate`?",
})

Build errors, such as a duplicate option or a level declared out of order, are reported by the call that sends the question rather than at construction, so questions can be package-level variables.

A ScoreAnswer's Legend holds each level's description as it was sent, so structured levels come back structured. Describe() renders the current level as text, JSON-encoding a structured one.

Client

client, err := systemone.New(
	systemone.WithAPIKey(key),          // default: $TYPESAFE_API_KEY
	systemone.WithBaseURL(url),         // default: $TYPESAFE_BASE_URL, then https://api.typesafe.ai
	systemone.WithModel("jev-1.13.0"),  // default: $TYPESAFE_DEFAULT_MODEL, then jev-latest
	systemone.WithHTTPClient(hc),
	systemone.WithTimeout(30*time.Second), // per attempt
	systemone.WithRetry(systemone.DefaultRetry()),
	systemone.WithHeader("X-Trace-Id", id),
	systemone.WithLogger(slog.Default()),
)

Client.Models lists the models the account can send. Client.Do takes a Request when one call needs to differ from the client's defaults:

res, err := client.Do(ctx, systemone.Request{
	State:     ticket,
	Questions: []systemone.Question{team, urgent, anger},
	Model:     "jev-1.13.0",
	Header:    http.Header{"X-Trace-Id": {id}},
	Retry:     &policy,              // nil inherits the client's
	Timeout:   45 * time.Second,     // per attempt; zero inherits, NoTimeout removes the bound
})

Give ctx a deadline to bound the whole call including retries; Timeout bounds one attempt.

Retries

The client does not retry. WithRetry(systemone.DefaultRetry()) turns on two retries with exponential backoff, jitter, and retry-after support; adjust the RetryPolicy fields or supply your own Retry predicate. Request.Retry sets a policy for one call, which is worth reaching for when a cheap three-question triage and an expensive two-hundred-question fan-out share a client. A retried request may be evaluated more than once by the service.

Errors

An unsuccessful response is a *systemone.Error carrying the status, the raw body, the response headers, and the x-typesafe-request-id. Classify it with errors.Is:

switch {
case errors.Is(err, systemone.ErrRateLimit):     // 429
case errors.Is(err, systemone.ErrUnprocessable): // 422, body names the field
case errors.Is(err, systemone.ErrOverloaded):    // 529
case errors.Is(err, systemone.ErrServer):        // any 5xx
}
Logging

The client is silent until you give it a logger. WithLogger(log) writes one log/slog record per HTTP attempt at the level in $TYPESAFE_LOG_LEVEL (debug, info, warn, error, off; default info), which WithLogLevel overrides.

At info each record is a summary: method, URL, attempt, duration, status, and request ID. At debug it also carries headers and bodies. Credential headers are redacted; bodies are not, and the request body holds your state.

Forward compatibility

Two escape hatches let you use an API feature before this package models it. Both skip every check the typed path applies, so upgrade instead when you can.

Request.Extra adds top-level request fields. A key that collides with state, model or questions is an error rather than an override.

res, err := client.Do(ctx, systemone.Request{
	State:     ticket,
	Questions: []systemone.Question{team},
	Extra:     map[string]any{"beam_width": 4},
})

RawQuestion sends a question shape this package does not model. Its answer comes back as undecoded JSON, and Response.Raw() gives you the whole body, which is how you reach answer kinds the typed path skips.

q := systemone.RawQuestion{
	QuestionID: "tally",
	Body:       map[string]any{"type": "tally", "instructions": "Count the line items"},
}
res, err := client.Ask(ctx, invoice, q)
answer, err := q.From(res) // json.RawMessage

Contributing

CI runs on every pull request: build and race tests on Go 1.24 and current stable, golangci-lint (which covers go vet, staticcheck, errcheck, unused and gofmt), a go mod tidy check, and govulncheck.

To run the same checks locally:

go test -race ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
go run golang.org/x/vuln/cmd/govulncheck@latest ./...

Releases and the versioning policy are described in CONTRIBUTING.md.

Examples

examples/triage is the support-ticket walkthrough from the quick start, written with this SDK: one request, three questions, routing in code.

export TYPESAFE_API_KEY=...
go run ./examples/triage

Patterns

pattern composes answers the way the TypeSafe docs describe. It is ordinary code over answers this package returns; the thresholds and weights are yours.

  • Thresholds.Band splits confidence into low, medium, and high so riskier actions can sit behind a higher bar.
  • Composite takes the weighted mean of normalized Weigh/WeighNoul terms. Changing a weight reranks existing answers without a new request.
  • Walk classifies down a Tree one Choice per level, showing each option's subtree so the model can see what lives under a branch before committing. Every step carries the full sibling distribution, so a beam search over close branches is a few lines on top.

License

MIT. See LICENSE.

Documentation

Overview

Package systemone is a client for the TypeSafe System One API.

A request sends one state and a set of typed questions; the response carries one answer per question. Questions are values you declare once and reuse:

type Dept string

const (
	Billing   Dept = "billing"
	Technical Dept = "technical"
)

var (
	dept = systemone.NewChoice[Dept]("department", "Which team should handle this?",
		systemone.Option(Billing, "Payments, invoicing, refunds"),
		systemone.Option(Technical, "Bugs, outages, integrations"),
	)
	urgent = systemone.NewNoul("is_urgent", "Does this convey urgency?")
)

res, err := client.Ask(ctx, "Help! My payouts have been failing for 3 days.", dept, urgent)
if err != nil {
	return err
}
d, err := dept.From(res)   // ChoiceAnswer[Dept]
u, err := urgent.From(res) // NoulAnswer

Reading an answer through its question handle is the only way to reach it, so the option type, the level type, and the answer type are all checked at compile time.

See https://docs.typesafe.ai for the concepts behind state, questions, probabilities, and confidence.

This is an unofficial, community-maintained client, not built, endorsed, or supported by TypeSafe.

Example

Independent questions about the same state go in one request. They are answered in parallel and cannot see one another's answers.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var (
	team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
		systemone.Option(TeamBilling, "Payments, invoicing, refunds"),
		systemone.Option(TeamTechnical, "Bugs, outages, integrations"),
		systemone.Option(TeamSales, "Pricing, upgrades, new accounts"),
	)
	timeSensitive = systemone.NewNoul("time_sensitive", "Does this convey urgency?",
		systemone.Yes("Explicitly time-sensitive"),
		systemone.No("No urgency expressed"),
	)
	anger = systemone.NewScore[Anger]("anger", "How frustrated is the customer?",
		systemone.Levels[Anger]("Calm and matter-of-fact", "Visibly frustrated", "Angry, threatening to leave"),
	)
)

func main() {
	client, err := systemone.New()
	if err != nil {
		log.Fatal(err)
	}

	ticket := map[string]any{
		"subject": "Payouts failing",
		"body":    "Help! My payouts have been failing for 3 days and nobody has replied.",
		"plan":    "enterprise",
	}

	res, err := client.Ask(context.Background(), ticket, team, timeSensitive, anger)
	if err != nil {
		log.Fatal(err)
	}

	t, err := team.From(res)
	if err != nil {
		log.Fatal(err)
	}
	u, err := timeSensitive.From(res)
	if err != nil {
		log.Fatal(err)
	}
	a, err := anger.From(res)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(t.Value, t.Confidence, u.Yes(0.8), a.Level, a.Describe())
}
Example (CompositeScoring)

Score each dimension on its own, then combine with weights your code owns. Changing a weight reranks existing answers without a new request.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/pattern"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var (
	timeSensitive = systemone.NewNoul("time_sensitive", "Does this convey urgency?",
		systemone.Yes("Explicitly time-sensitive"),
		systemone.No("No urgency expressed"),
	)
	anger = systemone.NewScore[Anger]("anger", "How frustrated is the customer?",
		systemone.Levels[Anger]("Calm and matter-of-fact", "Visibly frustrated", "Angry, threatening to leave"),
	)
)

func main() {
	client, err := systemone.New()
	if err != nil {
		log.Fatal(err)
	}
	res, err := client.Ask(context.Background(), "resume text", anger, timeSensitive)
	if err != nil {
		log.Fatal(err)
	}
	a, _ := anger.From(res)
	u, _ := timeSensitive.From(res)

	priority := pattern.Composite(
		pattern.Weigh(a, 0.7),
		pattern.WeighNoul(u, 0.3),
	)
	fmt.Printf("priority %.2f\n", priority)
}
Example (ConfidenceGatedRouting)

Confidence says whether to act on an answer, not whether the answer is right. Gate riskier actions at a higher bar.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/pattern"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
	systemone.Option(TeamBilling, "Payments, invoicing, refunds"),
	systemone.Option(TeamTechnical, "Bugs, outages, integrations"),
	systemone.Option(TeamSales, "Pricing, upgrades, new accounts"),
)

func main() {
	client, err := systemone.New()
	if err != nil {
		log.Fatal(err)
	}
	res, err := client.Ask(context.Background(), "Move everything into savings.", team)
	if err != nil {
		log.Fatal(err)
	}
	t, err := team.From(res)
	if err != nil {
		log.Fatal(err)
	}

	switch (pattern.Thresholds{Medium: 0.6, High: 0.85}).Band(t.Confidence) {
	case pattern.High:
		fmt.Println("route to", t.Value)
	case pattern.Medium:
		fmt.Println("confirm with the customer, then route to", t.Value)
	case pattern.Low:
		fmt.Println("hand to a support agent")
	}
}

Index

Examples

Constants

View Source
const (
	APIKeyEnv       = "TYPESAFE_API_KEY"
	BaseURLEnv      = "TYPESAFE_BASE_URL"
	DefaultModelEnv = "TYPESAFE_DEFAULT_MODEL"
	LogLevelEnv     = "TYPESAFE_LOG_LEVEL"
)

Environment variables read when the matching option is not given.

View Source
const (
	DefaultBaseURL = "https://api.typesafe.ai"
	DefaultModel   = "jev-latest"
	DefaultTimeout = 10 * time.Second
)

Client defaults.

View Source
const (
	MinScoreLevels = 2
	MaxScoreLevels = 10
)

Score level count limits the API enforces.

View Source
const LevelOff = slog.Level(math.MaxInt32)

LevelOff disables logging. It sits above every level slog defines, so no record is ever enabled.

View Source
const MaxChoiceOptions = 255

MaxChoiceOptions is the largest number of options the API accepts on one Choice question.

View Source
const NoTimeout = -1 * time.Nanosecond

NoTimeout removes the per-attempt bound from a call when given as a Request.Timeout. Any negative duration does the same.

Variables

View Source
var (
	ErrInvalidRequest   = errors.New("systemone: invalid request")
	ErrAuthentication   = errors.New("systemone: authentication failed")
	ErrPermissionDenied = errors.New("systemone: permission denied")
	ErrNotFound         = errors.New("systemone: not found")
	ErrUnprocessable    = errors.New("systemone: request failed validation")
	ErrRateLimit        = errors.New("systemone: rate limit exceeded")
	ErrOverloaded       = errors.New("systemone: service overloaded")
	ErrServer           = errors.New("systemone: server error")
)

Sentinels for classifying an *Error with errors.Is:

if errors.Is(err, systemone.ErrRateLimit) { ... }

ErrServer also matches ErrOverloaded, since 529 is a server status.

View Source
var (
	// ErrNoAnswer means the response carries no answer under the question's ID.
	ErrNoAnswer = errors.New("no answer for question")
	// ErrAnswerType means the answer under that ID is of a different primitive.
	ErrAnswerType = errors.New("answer is of a different question type")
	// ErrIncompleteAnswer means the answer arrived without a field the API
	// documents as required, so it cannot be read without inventing a value.
	ErrIncompleteAnswer = errors.New("answer is missing a required field")
)

Errors returned when reading an answer out of a Response.

View Source
var ErrNoAPIKey = errors.New("systemone: no API key")

ErrNoAPIKey is returned by New when no API key is given and APIKeyEnv is unset.

Functions

This section is empty.

Types

type Choice

type Choice[T ~string] struct {
	// contains filtered or unexported fields
}

Choice asks the model to pick one option from a set you define. T is the Go type of the options, so both the declared set and the answer are checked at compile time:

type Dept string

const (
	Billing   Dept = "billing"
	Technical Dept = "technical"
)

var dept = systemone.NewChoice[Dept]("department", "Which team should handle this?",
	systemone.Option(Billing, "Payments, invoicing, refunds"),
	systemone.Option(Technical, "Bugs, outages, integrations"),
)

See https://docs.typesafe.ai/primitives/choice.

func NewChoice

func NewChoice[T ~string](id string, instructions Content, opts ...ChoiceOption[T]) *Choice[T]

NewChoice builds a Choice question. A build error, such as a duplicate option, is reported by the Client call that sends the question rather than here, so questions can be declared as package-level variables.

func (*Choice[T]) From

func (q *Choice[T]) From(r *Response) (ChoiceAnswer[T], error)

From reads this question's answer out of a response. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID, or ErrAnswerType when that answer is a different primitive.

func (*Choice) ID

func (q *Choice) ID() string

func (*Choice[T]) Options

func (q *Choice[T]) Options() []T

Options returns the declared options in declaration order.

type ChoiceAnswer

type ChoiceAnswer[T ~string] struct {
	// Value is the highest-probability option.
	Value T
	// Probabilities maps every declared option to its probability. They sum to 1.
	Probabilities map[T]float64
	// Confidence is how concentrated Probabilities is, from 0 to 1. It says
	// whether to act on Value, not whether Value is correct. See
	// https://docs.typesafe.ai/confidence.
	Confidence float64
}

ChoiceAnswer is the answer to a Choice question.

func (ChoiceAnswer[T]) Probability

func (a ChoiceAnswer[T]) Probability(option T) float64

Probability returns the probability of one option, or 0 if the answer does not carry it.

func (ChoiceAnswer[T]) Ranked

func (a ChoiceAnswer[T]) Ranked() []T

Ranked returns the options ordered by descending probability. Equal probabilities are ordered by option name so the result is stable.

type ChoiceOption

type ChoiceOption[T ~string] interface {
	// contains filtered or unexported methods
}

ChoiceOption configures a Choice during construction.

func Option

func Option[T ~string](value T, description Content) ChoiceOption[T]

Option declares one selectable option and the rubric that describes it. A nil description sends the option with no extra detail. The description may be a string or any JSON-encodable structure; see https://docs.typesafe.ai/primitives/advanced.

func Options

func Options[T ~string](values ...T) ChoiceOption[T]

Options declares several options at once, none of them described. Use it when the option names carry the whole meaning.

type Client

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

Client sends requests to the System One API. It is safe for concurrent use.

func New

func New(opts ...ClientOption) (*Client, error)

New builds a client. Without options it reads the API key from APIKeyEnv, the base URL from BaseURLEnv, and the default model from DefaultModelEnv.

func (*Client) Ask

func (c *Client) Ask(ctx context.Context, state any, questions ...Question) (*Response, error)

Ask evaluates state against questions using the client's default model. It is Client.Do for the common case.

func (*Client) Do

func (c *Client) Do(ctx context.Context, req Request) (*Response, error)

Do sends one evaluation request.

func (*Client) Model

func (c *Client) Model() string

Model returns the model used for requests that do not name one.

func (*Client) Models

func (c *Client) Models(ctx context.Context) ([]Model, error)

Models lists the models the account can use. See https://docs.typesafe.ai/models.

type ClientOption

type ClientOption interface {
	// contains filtered or unexported methods
}

ClientOption configures a Client. Explicit options take precedence over environment variables, which take precedence over the defaults.

func WithAPIKey

func WithAPIKey(key string) ClientOption

WithAPIKey sets the API key, overriding APIKeyEnv.

func WithBaseURL

func WithBaseURL(rawURL string) ClientOption

WithBaseURL sets the API root, overriding BaseURLEnv. Endpoint paths are appended to it.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) ClientOption

WithHTTPClient sets the HTTP client used for every request. Use it to configure transport, proxies, or connection pooling. The client's own Timeout field, if set, bounds the whole call including retries, which is separate from WithTimeout.

func WithHeader

func WithHeader(key, value string) ClientOption

WithHeader adds a header sent on every request. A Request header of the same name replaces it for that request, except for Authorization, Accept, Content-Type and User-Agent, which the client always sets itself. WithUserAgent changes the last of those.

func WithLogLevel

func WithLogLevel(level slog.Level) ClientOption

WithLogLevel sets the level WithLogger logs at, overriding LogLevelEnv. Pass LevelOff to disable logging without removing the logger.

func WithLogger

func WithLogger(log *slog.Logger) ClientOption

WithLogger sends one record per HTTP attempt to log. The client logs nothing until this option is given, so it stays quiet inside a library by default.

The level comes from LogLevelEnv, which accepts debug, info, warn, error and off, and defaults to info; WithLogLevel overrides it. At info each record is a summary: method, URL, attempt, duration, status, and request ID. At debug it also carries headers and bodies.

Credential headers are redacted. Bodies are not, and the request body holds the state, so debug logging writes whatever you are evaluating to the log.

func WithModel

func WithModel(model string) ClientOption

WithModel sets the model used when a request does not name one, overriding DefaultModelEnv. See https://docs.typesafe.ai/models.

func WithRetry

func WithRetry(p RetryPolicy) ClientOption

WithRetry turns on retries. The client does not retry by default; pass DefaultRetry for a sensible policy.

Example

Retries are off by default. Turn them on with a policy you can tune.

package main

import (
	"log"
	"time"

	"github.com/justintout/systemone"
)

func main() {
	policy := systemone.DefaultRetry()
	policy.MaxRetries = 4
	policy.MaxBackoff = 10 * time.Second

	client, err := systemone.New(
		systemone.WithRetry(policy),
		systemone.WithTimeout(30*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}
	_ = client
}

func WithTimeout

func WithTimeout(d time.Duration) ClientOption

WithTimeout bounds one attempt, not the whole call. A zero duration removes the bound. The default is DefaultTimeout.

func WithUserAgent

func WithUserAgent(ua string) ClientOption

WithUserAgent replaces the default User-Agent header.

type Content

type Content = any

Content is the body of a question's instructions or of a single criterion. A string covers most questions. Any value that encodes to a JSON object or array is also accepted, which lets a question carry named data it refers to by backticked path. See https://docs.typesafe.ai/primitives/advanced.

type Error

type Error struct {
	// StatusCode is the HTTP status code.
	StatusCode int
	// RequestID is the x-typesafe-request-id response header, empty if absent.
	RequestID string
	// Body is the raw response body, which describes the offending field on a
	// 422. It is not parsed, because the API does not contract its shape.
	Body []byte
	// Header is the response header.
	Header http.Header
	// RetryAfter is the wait the server asked for, or 0 if it asked for none.
	RetryAfter time.Duration
}

Error is an unsuccessful HTTP response from the API.

Example

Classify an HTTP failure by its status.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/justintout/systemone"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
	systemone.Option(TeamBilling, "Payments, invoicing, refunds"),
	systemone.Option(TeamTechnical, "Bugs, outages, integrations"),
	systemone.Option(TeamSales, "Pricing, upgrades, new accounts"),
)

func main() {
	client, err := systemone.New()
	if err != nil {
		log.Fatal(err)
	}
	_, err = client.Ask(context.Background(), "state", team)

	var apiErr *systemone.Error
	switch {
	case err == nil:
	case errors.Is(err, systemone.ErrRateLimit):
		fmt.Println("back off and retry")
	case errors.Is(err, systemone.ErrUnprocessable):
		// The body names the offending field.
		errors.As(err, &apiErr)
		fmt.Println("bad question:", string(apiErr.Body))
	default:
		fmt.Println(err)
	}
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether the status this error carries is the class target names.

type Model

type Model struct {
	Name        string
	Description string
	// ReleaseDate is when the model or alias was released. It is the zero time
	// when the API omits it.
	//
	// The API sends RFC 3339, for example 2026-09-10T18:38:01.391457+00:00,
	// despite the SDK documentation showing a bare YYYY-MM-DD date.
	ReleaseDate time.Time
}

Model describes a model or alias the account may send in a request.

func (*Model) UnmarshalJSON

func (m *Model) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a model entry, parsing its release date, which the API sends as an RFC 3339 timestamp.

type Noul

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

Noul asks a yes/no question and returns the probability that the answer is yes. A value near 0.5 means yes and no are close to equally likely, not that the condition half holds; see https://docs.typesafe.ai/primitives/noul.

var urgent = systemone.NewNoul("is_urgent", "Does this convey urgency?",
	systemone.Yes("Explicitly time-sensitive"),
	systemone.No("No urgency expressed"),
)

Use one Noul per label when several labels may apply at once.

func NewNoul

func NewNoul(id string, instructions Content, opts ...NoulOption) *Noul

NewNoul builds a Noul question. Criteria are optional; describe both sides when the boundary between yes and no is subtle.

func (*Noul) From

func (q *Noul) From(r *Response) (NoulAnswer, error)

From reads this question's answer out of a response. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID, or ErrAnswerType when that answer is a different primitive.

func (*Noul) ID

func (q *Noul) ID() string

type NoulAnswer

type NoulAnswer struct {
	// Value is the probability that the answer is yes, from 0 to 1.
	Value float64
}

NoulAnswer is the answer to a Noul question. Noul answers carry no confidence: the probability is the whole answer.

func (NoulAnswer) Yes

func (a NoulAnswer) Yes(threshold float64) bool

Yes reports whether the probability of yes is at least threshold. Pick the threshold from your own data and the cost of being wrong.

type NoulOption

type NoulOption interface {
	// contains filtered or unexported methods
}

NoulOption configures a Noul during construction.

func No

func No(description Content) NoulOption

No describes what a no, a value near 0, means.

func Yes

func Yes(description Content) NoulOption

Yes describes what a yes, a value near 1, means.

type Question

type Question interface {
	// ID is the key the question is sent under and the answer comes back under.
	ID() string
	// contains filtered or unexported methods
}

Question is one typed question in a request. The three implementations are Choice, Score and Noul; the interface is closed.

type QuestionError

type QuestionError struct {
	QuestionID string
	Err        error
}

QuestionError reports a question that cannot be sent as built.

func (*QuestionError) Error

func (e *QuestionError) Error() string

func (*QuestionError) Unwrap

func (e *QuestionError) Unwrap() error

type RawQuestion

type RawQuestion struct {
	QuestionID string
	Body       Content
}

RawQuestion sends a question this package does not model, so a new API feature can be used before an SDK release adds typed support for it. Body is the whole question object, including its type field:

beam := systemone.RawQuestion{
	QuestionID: "department",
	Body: map[string]any{
		"type":         "choice",
		"instructions": "Which team should handle this?",
		"criteria":     map[string]any{"billing": nil, "technical": nil},
		"beam_width":   4,
	},
}

Nothing about Body is checked, and its answer comes back as undecoded JSON. Prefer NewChoice, NewNoul and NewScore, and upgrade the SDK when it grows support for what you need.

func (RawQuestion) From

func (q RawQuestion) From(r *Response) (json.RawMessage, error)

From returns this question's answer as undecoded JSON. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID.

func (RawQuestion) ID

func (q RawQuestion) ID() string

ID returns the key this question is sent under.

type Request

type Request struct {
	// State is the content to evaluate: a string, or any value that encodes to
	// a JSON object or array. See https://docs.typesafe.ai/concepts/state.
	State any
	// Questions are asked in one call and answered in parallel. They cannot see
	// one another's answers.
	Questions []Question
	// Model overrides the client default for this request.
	Model string
	// Header adds or replaces headers for this request. Authorization, Accept,
	// Content-Type and User-Agent are set by the client after this is applied,
	// so they cannot be overridden here; see [WithUserAgent].
	Header http.Header
	// Retry overrides the client's retry policy for this request. A nil Retry
	// inherits it; a zero RetryPolicy turns retries off for this call alone.
	Retry *RetryPolicy
	// Timeout overrides the client's per-attempt timeout for this request. Zero
	// inherits it, and [NoTimeout] removes the bound. To bound the whole call
	// including retries, give ctx a deadline.
	Timeout time.Duration
	// Extra adds top-level fields to the request body, for API features this
	// package does not model yet. A key that collides with state, model or
	// questions is an error rather than an override. Prefer upgrading the SDK.
	Extra map[string]any
}

Request is one evaluation: a state, the questions to ask about it, and how to send them.

type Response

type Response struct {
	// Model is the versioned model that answered, even when the request named
	// an alias such as jev-latest.
	Model string
	// Usage is the token count for the request.
	Usage Usage
	// RequestID is the x-typesafe-request-id response header, empty if absent.
	RequestID string
	// contains filtered or unexported fields
}

Response is the result of one evaluation. Answers are read through the question handles that produced them: Choice.From, Score.From, Noul.From.

func (*Response) IDs

func (r *Response) IDs() []string

IDs returns the question IDs the response carries, sorted. It includes IDs whose answers this package does not recognize, which a question the SDK predates can still produce.

func (*Response) Raw

func (r *Response) Raw() []byte

Raw returns the undecoded response body. Use it to reach answer kinds this package does not model yet. The caller must not modify the returned bytes.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is how many attempts follow the first one.
	MaxRetries int
	// InitialBackoff is the wait before the first retry. It doubles each
	// attempt up to MaxBackoff.
	InitialBackoff time.Duration
	// MaxBackoff caps the wait between attempts.
	MaxBackoff time.Duration
	// Jitter spreads each wait by up to this fraction in either direction, so
	// concurrent clients do not retry in lockstep. 0.25 means plus or minus a
	// quarter of the computed wait. It must be between 0 and 1: above 1 the
	// spread exceeds the delay and produces a negative wait.
	Jitter float64
	// Retry reports whether a response with this status should be retried. A
	// nil Retry retries 408, 429, and every 5xx.
	Retry func(status int) bool
	// RetryTransportError retries a request that failed without a response,
	// including a per-attempt timeout.
	RetryTransportError bool
	// RespectRetryAfter uses the server's retry-after header in place of the
	// computed backoff when the response carries one.
	RespectRetryAfter bool
}

RetryPolicy controls whether the client retries a failed attempt. The zero value performs no retries, which is the client default; pass DefaultRetry to WithRetry to turn retries on.

Retries apply to the whole request, including transport failures, so a retried request may be evaluated more than once by the service.

func DefaultRetry

func DefaultRetry() RetryPolicy

DefaultRetry is the policy to start from: two retries with exponential backoff from 500ms, capped at 5s, honoring retry-after.

type Score

type Score[T ~int] struct {
	// contains filtered or unexported fields
}

Score rates the state along ordered levels you define and returns a probability-weighted position across them. T is an integer type whose constants are the levels, declared from lowest to highest starting at zero:

type Frustration int

const (
	Calm Frustration = iota
	Frustrated
	VeryAngry
)

var frustration = systemone.NewScore[Frustration]("frustration", "How frustrated is the customer?",
	systemone.Level(Calm, "Calm"),
	systemone.Level(Frustrated, "Frustrated"),
	systemone.Level(VeryAngry, "Very angry"),
)

Each level description must describe a concrete situation and stand on its own. See https://docs.typesafe.ai/primitives/score.

func NewScore

func NewScore[T ~int](id string, instructions Content, opts ...ScoreOption[T]) *Score[T]

NewScore builds a Score question.

func (*Score[T]) From

func (q *Score[T]) From(r *Response) (ScoreAnswer[T], error)

From reads this question's answer out of a response. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID, or ErrAnswerType when that answer is a different primitive.

func (*Score) ID

func (q *Score) ID() string

func (*Score[T]) Levels

func (q *Score[T]) Levels() int

Levels returns the number of declared levels.

type ScoreAnswer

type ScoreAnswer[T ~int] struct {
	// Value is the probability-weighted position across the levels. It can land
	// between two levels, so it is a float rather than a T.
	Value float64
	// Level is the single most probable level. It can differ from rounding
	// Value when the distribution is lopsided.
	Level T
	// Probabilities maps every level to its probability. They sum to 1.
	Probabilities map[T]float64
	// Legend maps every level back to the description that was sent for it.
	// The API echoes the description as it was sent, so a level declared with
	// a structured description comes back structured. A response may carry
	// fewer entries than the question declared, so it does not give the scale.
	Legend map[T]Content
	// LevelCount is how many levels the question declared. It comes from the
	// question, not the response, and is what [ScoreAnswer.Normalized] divides
	// by. A ScoreAnswer built by hand must set it.
	LevelCount int
	// Confidence is how concentrated Probabilities is, from 0 to 1. See
	// https://docs.typesafe.ai/confidence.
	Confidence float64
}

ScoreAnswer is the answer to a Score question.

func (ScoreAnswer[T]) Describe

func (a ScoreAnswer[T]) Describe() string

Describe returns the description ScoreAnswer.Level was sent with, as text. A level declared with a structured description is rendered as JSON; reach into ScoreAnswer.Legend to read its fields.

func (ScoreAnswer[T]) Normalized

func (a ScoreAnswer[T]) Normalized() float64

Normalized maps Value onto 0 to 1, dividing by the highest declared level. Use it to put scores with different level counts on one scale before combining them. It returns 0 when LevelCount is unset, which only happens on a ScoreAnswer that did not come from Score.From.

func (ScoreAnswer[T]) Probability

func (a ScoreAnswer[T]) Probability(level T) float64

Probability returns the probability of one level, or 0 if the answer does not carry it.

type ScoreOption

type ScoreOption[T ~int] interface {
	// contains filtered or unexported methods
}

ScoreOption configures a Score during construction.

func Level

func Level[T ~int](level T, description Content) ScoreOption[T]

Level declares one level and its description. Levels are sent as an ordered array, so level must equal the position it is declared in: the first Level must be 0, the second 1, and so on. A mismatch fails the request rather than silently shifting the scale.

func Levels

func Levels[T ~int](descriptions ...Content) ScoreOption[T]

Levels declares every level at once, in order from lowest to highest. The level values are the positions in the list.

type Usage

type Usage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

Usage is the token count the request was billed on. Only input tokens are charged; see https://docs.typesafe.ai/models.

Directories

Path Synopsis
examples
triage command
Command triage is the support-ticket example from the TypeSafe quick start, written with this SDK.
Command triage is the support-ticket example from the TypeSafe quick start, written with this SDK.
internal
releasebot command
Command releasebot works out whether the changes since the last release are worth publishing, and whether they may break existing callers.
Command releasebot works out whether the changes since the last release are worth publishing, and whether they may break existing callers.
Package pattern composes System One answers into the shapes the TypeSafe docs describe: confidence-gated routing, composite scoring, and walking a taxonomy one Choice at a time.
Package pattern composes System One answers into the shapes the TypeSafe docs describe: confidence-gated routing, composite scoring, and walking a taxonomy one Choice at a time.

Jump to

Keyboard shortcuts

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