first commit

This commit is contained in:
Doar Mihai
2026-10-16 19:35:49 +03:00
commit 53a9c115f8
12 changed files with 4872 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "liquidsky/laya_php_sdk",
"description": "PHP SDK for Laya, the open-source System 1 decision engine",
"type": "library",
"require": {
"pestphp/pest": "^5.3",
"phpstan/phpstan": "^2.3"
},
"require-dev": {
"php-http/discovery": "^1.20",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.1"
},
"license": "MIT",
"autoload": {
"psr-4": {
"Laya\\": "src/"
}
},
"authors": [
{
"name": "Doar Mihai",
"email": "mihai@invata-progremare.ro"
}
],
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
}
}
Generated
+4564
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
<?php
namespace Laya\Exception;
final class UnexpectedResponseException extends \RuntimeException
{
public function __construct(string $message, private readonly int $statusCode)
{
parent::__construct($message, $statusCode);
}
public function statusCode(): int
{
return $this->statusCode;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
// src/Laya.php
namespace Laya;
use Laya\Question\QuestionSet;
final class Laya
{
public static function client(?string $baseUrl = null): LayaClient
{
return \Http\Discovery\Psr18ClientDiscovery::find() instanceof \Psr\Http\Client\ClientInterface
? new LayaClient(
\Http\Discovery\Psr18ClientDiscovery::find(),
\Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(),
\Http\Discovery\Psr17FactoryDiscovery::findStreamFactory(),
$baseUrl ?? 'http://127.0.0.1:8000'
)
: throw new \RuntimeException('No PSR-18 client found. Install guzzlehttp/guzzle or nyholm/psr7.');
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
// src/LayaClient.php
namespace Laya;
use Laya\Exception\UnexpectedResponseException;
use Laya\Question\QuestionSet;
use Laya\Result\DecisionSet;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
final class LayaClient
{
public function __construct(
private readonly ClientInterface $http,
private readonly RequestFactoryInterface $requests,
private readonly StreamFactoryInterface $streams,
private readonly string $baseUrl = 'http://127.0.0.1:8000',
) {}
/**
* @param string|array<string, mixed> $state raw text or a state document
*/
public function decide(string|array $state, QuestionSet $questions, ?string $model = null): DecisionSet
{
$payload = [
'state' => is_string($state) ? ['body' => $state] : $state,
'questions' => $questions->toArray(),
];
if ($model !== null) {
$payload['model'] = $model;
}
return DecisionSet::fromResponse($this->post('/predict', $payload));
}
/** @return array<string, mixed> */
public function models(): array
{
return $this->get('/models');
}
/** @return array<string, mixed> */
private function post(string $path, array $payload): array
{
$request = $this->requests->createRequest('POST', $this->baseUrl . $path)
->withHeader('Content-Type', 'application/json')
->withHeader('Accept', 'application/json')
->withBody($this->streams->createStream(json_encode($payload, JSON_THROW_ON_ERROR)));
return $this->decode($this->http->sendRequest($request));
}
/** @return array<string, mixed> */
private function get(string $path): array
{
$request = $this->requests->createRequest('GET', $this->baseUrl . $path)
->withHeader('Accept', 'application/json');
return $this->decode($this->http->sendRequest($request));
}
/** @return array<string, mixed> */
private function decode(\Psr\Http\Message\ResponseInterface $response): array
{
$status = $response->getStatusCode();
$body = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400) {
$detail = $body['detail'] ?? json_encode($body);
throw new UnexpectedResponseException("Laya returned HTTP $status: $detail", $status);
}
return $body;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Laya\Question;
final class ChoiceQuestion implements Question
{
/** @var array<string, string> */
private array $criteria = [];
public function __construct(
private readonly string $name,
private readonly string $instructions,
) {}
public function option(string $label, string $description): static
{
$this->criteria[$label] = $description;
return $this;
}
public function name(): string { return $this->name; }
public function toArray(): array
{
return [
'type' => 'choice',
'instructions' => $this->instructions,
'criteria' => $this->criteria,
];
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Laya\Question;
interface Question
{
public function name(): string;
public function toArray(): array; // {type, instructions, criteria?}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Laya\Question;
final class QuestionSet
{
/** @var array<string, Question> */
private array $questions = [];
public static function make(): self { return new self(); }
public function add(Question $question): static
{
$this->questions[$question->name()] = $question;
return $this;
}
/** @return array<string, array> */
public function toArray(): array
{
$out = [];
foreach ($this->questions as $name => $q) {
$out[$name] = $q->toArray();
}
return $out;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Laya\Question;
final class ScoreQuestion implements Question
{
/** @param list<string> $scale */
public function __construct(
private readonly string $name,
private readonly string $instructions,
private readonly array $scale, // e.g. ['not urgent', 'soon', 'critical']
) {}
public function name(): string { return $this->name; }
public function toArray(): array
{
return [
'type' => 'score',
'instructions' => $this->instructions,
'criteria' => $this->scale,
];
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Laya\Question;
final class YesNoQuestion implements Question
{
public function __construct(
private readonly string $name,
private readonly string $instructions,
) {}
public function name(): string { return $this->name; }
public function toArray(): array
{
return ['type' => 'noul', 'instructions' => $this->instructions];
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Laya\Result;
final class Decision
{
public function __construct(
public readonly string $name,
public readonly string $type, // choice|score|noul
private readonly array $answer, // raw answers.<name> payload
) {}
/** For choice questions: the chosen label */
public function choice(): ?string { return $this->answer['choice'] ?? null; }
/** For noul questions: P(yes) */
public function probability(): ?float {
return isset($this->answer['noul']) ? (float) $this->answer['noul'] : null;
}
/** For score questions */
public function score(): ?float { return isset($this->answer['score']) ? (float) $this->answer['score'] : null; }
public function confidence(): ?float { return isset($this->answer['confidence']) ? (float) $this->answer['confidence'] : null; }
/** Full probability distribution */
public function distribution(): array { return $this->answer['distribution'] ?? $this->answer; }
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Laya\Result;
final class DecisionSet
{
/** @param array<string, Decision> $decisions */
private function __construct(
private readonly array $decisions,
public readonly ?string $routedModel = null,
) {}
public static function fromResponse(array $response): self
{
$decisions = [];
foreach ($response['answers'] ?? [] as $name => $answer) {
$decisions[$name] = new Decision($name, $answer['type'] ?? 'unknown', $answer);
}
return new self($decisions, $response['routing']['model'] ?? null);
}
public function get(string $name): Decision
{
return $this->decisions[$name]
?? throw new \OutOfBoundsException("No answer for question '$name'");
}
}