Laya PHP SDK

PHP client for Laya, the open-source System 1 decision engine. You give Laya some state (free text or a structured document) and a set of questions. It answers each question with a choice, a score or a yes/no probability, plus a confidence value.

The SDK is a thin layer over Laya's HTTP API. It builds the JSON request, sends it over any PSR-18 HTTP client, and wraps the response in small value objects.

Requirements

  • PHP 8.1+ (uses readonly properties and never-style throw expressions)
  • A running Laya server (the default URL is http://127.0.0.1:8000)
  • An HTTP client: Guzzle is installed with the package. If your app already uses another PSR-18 client (Symfony HttpClient, etc.), the SDK discovers and prefers it through php-http/discovery.

Installation

composer require liquidsky/laya_php_sdk

Quick start

use Laya\Laya;
use Laya\Question\ChoiceQuestion;
use Laya\Question\QuestionSet;
use Laya\Question\ScoreQuestion;
use Laya\Question\YesNoQuestion;

$client = Laya::client('http://127.0.0.1:8000');

$questions = QuestionSet::make()
    ->add(
        (new ChoiceQuestion('category', 'What is this ticket about?'))
            ->option('billing', 'Payments, invoices, refunds')
            ->option('bug', 'Something is broken')
            ->option('other', 'Anything else')
    )
    ->add(new ScoreQuestion('urgency', 'How urgent is this?', ['not urgent', 'soon', 'critical']))
    ->add(new YesNoQuestion('angry', 'Is the customer upset?'));

$decisions = $client->decide('My card was charged twice and nobody replies!!', $questions);

$decisions->get('category')->choice();       // "billing"
$decisions->get('urgency')->score();         // e.g. 2.0
$decisions->get('angry')->probability();     // e.g. 0.93  (P(yes))
$decisions->get('angry')->confidence();      // e.g. 0.88
$decisions->routedModel;                     // model Laya routed to, if reported

How it works

 state + QuestionSet ──► LayaClient::decide() ──► POST /predict ──► Laya server
                                                                        │
 DecisionSet / Decision ◄── DecisionSet::fromResponse() ◄── JSON ◄──────┘
  1. Build questions. Each question is an object implementing Question (name() and toArray()). A QuestionSet collects them, keyed by name.

  2. Call decide(). LayaClient::decide() sends this body to POST /predict:

    {
      "state": { "body": "My card was charged twice..." },
      "questions": {
        "category": { "type": "choice", "instructions": "...", "criteria": { "billing": "..." } },
        "urgency":  { "type": "score",  "instructions": "...", "criteria": ["not urgent", "soon", "critical"] },
        "angry":    { "type": "noul",   "instructions": "..." }
      },
      "model": "optional-model-name"
    }
    

    A string $state is wrapped as {"body": "<string>"}. An array is sent as-is. model is only included if you pass it.

  3. Read the answers. The response's answers.<name> entries become Decision objects inside a DecisionSet. routing.model is exposed as $decisions->routedModel.

Question types

Class Wire type Constructor / builder Read with
ChoiceQuestion choice new ChoiceQuestion($name, $instructions) then ->option($label, $description) choice()
ScoreQuestion score new ScoreQuestion($name, $instructions, list<string> $scale) score()
YesNoQuestion noul new YesNoQuestion($name, $instructions) probability()

noul is the name Laya's API uses for a yes/no question. The value is the probability of "yes".

To add your own type, implement Laya\Question\Question.

Reading results

$decisions->get($name) returns a Decision, or throws OutOfBoundsException if the server returned no answer for that name.

Member Returns
$decision->name, $decision->type Question name and the type reported by the server
choice() Chosen label (choice questions), else null
score() Score as float (score questions), else null
probability() P(yes) as float (noul questions), else null
confidence() Confidence as float, or null
distribution() Full probability distribution from the response

Other endpoints

$client->models();   // GET /models, returns the decoded JSON as an array

Errors

  • HTTP status 400 or above throws Laya\Exception\UnexpectedResponseException. Its message includes the server's detail field when present, and statusCode() returns the HTTP status.
  • Invalid JSON in a response or request throws JsonException.
  • Network failures surface as whatever your PSR-18 client throws (Psr\Http\Client\ClientExceptionInterface).
  • Laya::client() throws if no PSR-18 client or PSR-17 factory can be discovered.

Using your own HTTP client

Laya::client() auto-discovers dependencies. To control them (timeouts, auth headers, middleware), construct the client yourself:

use Laya\LayaClient;

$client = new LayaClient(
    $psr18Client,          // Psr\Http\Client\ClientInterface
    $psr17RequestFactory,  // Psr\Http\Message\RequestFactoryInterface
    $psr17StreamFactory,   // Psr\Http\Message\StreamFactoryInterface
    'https://laya.example.com',
);

Configure timeouts and authentication on the PSR-18 client you pass in. The SDK adds only Content-Type and Accept headers.

Project layout

src/
├── Laya.php                  Factory: Laya::client() with auto-discovery
├── LayaClient.php            decide(), models(); builds and sends requests
├── Exception/
│   └── UnexpectedResponseException.php
├── Question/
│   ├── Question.php          Interface
│   ├── QuestionSet.php       Named collection, serialises to the "questions" object
│   ├── ChoiceQuestion.php
│   ├── ScoreQuestion.php
│   └── YesNoQuestion.php
└── Result/
    ├── DecisionSet.php       Parsed response, lookup by question name
    └── Decision.php          One answer

Known issues

See CODE_REVIEW.md for open review findings. Note that its findings about the autoload namespace and inverted require sections have been fixed.

License

MIT

S
Description
No description provided
Readme
89 KiB
Languages
PHP 100%