Skip to content

1.3.0

Choose a tag to compare

@marcreichel marcreichel released this 11 Oct 17:18
c4480d9

Decision classes get two new shapes: an enum can be a score scale, and an array of enum cases asks one yes/no question per case. laya-serve can now abstain on low-confidence answers itself, predictions dispatch PSR-14 events, and php artisan laya:try lets you test a decision class on a piece of text without writing code.

final class Triage
{
    public function __construct(
        #[Ask('How urgent is this?')]
        public Urgency $urgency,              // #[Scale] enum: Low, Normal, Blocking

        #[Ask('Does the message mention {case}?')]
        #[Of(Topic::class)]
        public array $topics,                 // [Topic::Billing, Topic::Account]
    ) {}
}

$triage = $laya->decide($ticket, Triage::class, minConfidence: 0.7);

New

  • #[Of(Enum::class)] on array parameters. laya asks one yes/no question per case and fills {case} in the question with the case's #[Describe] text or its value. You get the cases with P(yes) ≥ 0.5 (or your threshold), in declaration order. The questions have ids like topics.billing. A class that needs more than 64 questions throws an InvalidQuestionException.
  • #[Scale] enums. A backed enum marked #[Scale] becomes a score question. Its cases are the levels, lowest first, and you get back the case at the most likely level instead of an index. A backed enum without #[Scale] stays a choice question.
  • Server-side abstention. predict(), predictMany(), decide() and decideMany() take minConfidence. Pass one threshold from 0 to 1, or a map with a threshold per option-count bucket (['choice:3-5' => 0.6, 'noul:2' => 0.8, 'default' => 0.7]). Answers report what the gate decided in abstention (the new Abstention enum), abstentionThreshold and lowConfidence. A malformed value throws the new InvalidOptionException before anything is sent.
  • PSR-14 events. Pass a dispatcher as events: to get PredictionMade after each prediction (cache hits included), PredictionFailed right before an exception is thrown, and CacheFailed when the cache misbehaves. States are left out of the events unless you set includeState. In Laravel, the service provider wires up the app's dispatcher, so listeners and Event::fake() work without any setup. Turn events off with LAYA_EVENTS=false, or include states with LAYA_EVENTS_INCLUDE_STATE=true.
  • php artisan laya:try. It runs a decision class on text from an argument, --file or STDIN. For each parameter it prints the question, the hydrated value, the answer confidence and a bar for every option's probability. It also shows the routed checkpoint and warns when the text was cut off. It takes --model, --max-len, --head-max-len and --json, and resolves short class names under App\.
  • Idle unload in laya:health. HealthStatus exposes idleUnloadSeconds and idleSeconds, and laya:health prints them. When laya-serve has unloaded its checkpoints after a quiet period, an empty "Loaded" list now comes with the reason.
  • More Laya::fake().
    • when() gives states that match a callback their own answers.
    • sequence() hands out answers in order.
    • assertNotPredicted() and assertNotDecided() check that something didn't happen.
    • The fake dispatches events, answers #[Of] and #[Scale] parameters, and applies minConfidence the way laya-serve does.
  • New example. examples/07-model-routing.php routes each prompt to the cheapest Claude model that can handle it.

Improved

  • The cache is best-effort. If a cache read throws or returns something that isn't a laya response, Laya treats it as a miss. If a write throws, you still get the result, just uncached. Both dispatch CacheFailed.
  • Identical states in one predictMany() call are sent once. Each copy still gets its own result and its own PredictionMade.
  • Decision classes are reflected once per process. decide(), decideMany() and assertDecided() share one cached reading of each class.
  • A PredictionFailed listener that throws no longer hides the laya error. You still get the original LayaException, such as ServerBusyException.

Upgrading

There are no breaking API changes, but a few things behave differently:

  • Responses that don't fit the decision class throw ServerException. When laya-serve's answers are missing, have the wrong type or carry a label outside the enum, decide() and decideMany() used to throw OutOfBoundsException, UnexpectedValueException or ValueError. They now throw a ServerException that names the parameter and keeps the original exception as previous. Catch LayaException to handle these along with the other laya errors.
  • Events are on by default in Laravel. If you listen for nothing, they cost next to nothing. To turn them off, set LAYA_EVENTS=false.
  • psr/simple-cache ^1.0 and ^2.0 are allowed as well as ^3.0.
  • Laya 0.4.0 changes the default routing. compose.yaml now builds laya v0.4.0 (3cf26cb). In 0.4.0, text whose language laya can't identify, such as very short text or text without letters, goes to the multilingual checkpoint instead of english. If your traffic is mostly English, start laya-serve with LAYA_DEFAULT_MODEL=english to keep the old behaviour. Laya::fake() now reports multilingual when no model is pinned.

Each feature needs a minimum laya-serve version:

Feature laya-serve
minConfidence per bucket, idle-unload fields 0.3.25
Multilingual default routing 0.4.0