first commit
This commit is contained in:
@@ -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
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Laya\Question;
|
||||||
|
|
||||||
|
interface Question
|
||||||
|
{
|
||||||
|
public function name(): string;
|
||||||
|
public function toArray(): array; // {type, instructions, criteria?}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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'");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user