diff --git a/composer.json b/composer.json index 9651b02..98892f9 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "test:lint": "@php vendor/bin/pint --config https://raw.githubusercontent.com/DIJ-digital/pint-config/main/pint.json", "test:unit": "pest", "test:types": "phpstan", - "test:refactor": "rector --dry-run", + "test:refactor": "rector", "test": [ "@test:lint", "@test:type-coverage", diff --git a/peck.json b/peck.json index 4b0e01d..902e556 100644 --- a/peck.json +++ b/peck.json @@ -1,6 +1,14 @@ { "preset": "base", "ignore": { - "words": ["php", "langfuse", "param", "compilable", "eval"] + "words": [ + "php", + "langfuse", + "param", + "compilable", + "eval", + "sha", + "tokenizer" + ] } } diff --git a/src/Contracts/TransporterInterface.php b/src/Contracts/TransporterInterface.php index 8234ade..558497a 100644 --- a/src/Contracts/TransporterInterface.php +++ b/src/Contracts/TransporterInterface.php @@ -9,34 +9,34 @@ interface TransporterInterface { /** - * @param array $options + * @param array $options */ public function request(string $method, string $uri, array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function get(string $uri, array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function post(string $uri, array $options = []): ResponseInterface; /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options */ public function postJson(string $uri, array $data = [], array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function delete(string $uri, array $options = []): ResponseInterface; /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options */ public function patchJson(string $uri, array $data = [], array $options = []): ResponseInterface; } diff --git a/src/Langfuse.php b/src/Langfuse.php index ebe96f9..1e8b65e 100644 --- a/src/Langfuse.php +++ b/src/Langfuse.php @@ -12,8 +12,7 @@ public function __construct( private readonly TransporterInterface $transporter, private readonly string $environment = 'default', private readonly string $label = 'latest', - ) { - } + ) {} public function prompt(): Prompt { @@ -37,4 +36,11 @@ public function score(): Score transporter: $this->transporter, ); } + + public function session(): Session + { + return new Session( + transporter: $this->transporter, + ); + } } diff --git a/src/Responses/SessionListResponse.php b/src/Responses/SessionListResponse.php new file mode 100644 index 0000000..43ffe24 --- /dev/null +++ b/src/Responses/SessionListResponse.php @@ -0,0 +1,39 @@ + $data + */ + public function __construct( + public array $data, + public MetaData $meta, + ) {} + + /** + * @param array{ + * data: array, + * meta: array{page: int, limit: int, totalPages: int, totalItems: int} + * } $data + */ + public static function fromArray(array $data): self + { + return new self( + data: array_map(fn (array $item): SessionResponse => SessionResponse::fromArray($item), $data['data']), + meta: MetaData::fromArray($data['meta']), + ); + } +} diff --git a/src/Responses/SessionResponse.php b/src/Responses/SessionResponse.php new file mode 100644 index 0000000..9c27618 --- /dev/null +++ b/src/Responses/SessionResponse.php @@ -0,0 +1,45 @@ + $traces + */ + public function __construct( + public string $id, + public string $createdAt, + public string $projectId, + public ?string $environment = null, + public ?bool $bookmarked = null, + public ?bool $public = null, + public array $traces = [], + ) {} + + /** + * @param array{ + * id: string, + * createdAt: string, + * projectId: string, + * environment?: string|null, + * bookmarked?: bool|null, + * public?: bool|null, + * traces?: array + * } $data + */ + public static function fromArray(array $data): self + { + return new self( + id: $data['id'], + createdAt: $data['createdAt'], + projectId: $data['projectId'], + environment: $data['environment'] ?? null, + bookmarked: $data['bookmarked'] ?? null, + public: $data['public'] ?? null, + traces: $data['traces'] ?? [], + ); + } +} diff --git a/src/Session.php b/src/Session.php new file mode 100644 index 0000000..5663e10 --- /dev/null +++ b/src/Session.php @@ -0,0 +1,77 @@ +transporter->get( + uri: sprintf('/api/public/sessions/%s', urlencode($sessionId)), + ); + + /** @var array{ + * id: string, + * createdAt: string, + * projectId: string, + * environment: string|null, + * bookmarked: bool|null, + * public: bool|null, + * traces: array + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return SessionResponse::fromArray($data); + } + + /** + * @throws JsonException + */ + public function list( + ?int $page = null, + ?int $limit = null, + ?string $fromTimestamp = null, + ?string $toTimestamp = null, + ?array $environment = null, + ): SessionListResponse { + $response = $this->transporter->get( + uri: '/api/public/sessions', + options: ['query' => array_filter([ + 'page' => $page, + 'limit' => $limit, + 'fromTimestamp' => $fromTimestamp, + 'toTimestamp' => $toTimestamp, + 'environment' => $environment, + ])] + ); + + /** @var array{ + * data: array, + * meta: array{page: int, limit: int, totalPages: int, totalItems: int} + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return SessionListResponse::fromArray($data); + } +} diff --git a/src/Testing/Responses/GetSessionListResponse.php b/src/Testing/Responses/GetSessionListResponse.php new file mode 100644 index 0000000..34b4d85 --- /dev/null +++ b/src/Testing/Responses/GetSessionListResponse.php @@ -0,0 +1,51 @@ +|string> $headers + */ + public function __construct(int $status = 200, array $headers = [], string $version = '1.1', ?string $reason = null) + { + parent::__construct($status, $headers, (string) json_encode($this->payload()), $version, $reason); + } + + /** + * @return array + */ + public function payload(): array + { + return [ + 'data' => [ + [ + 'id' => 'session-abc123', + 'createdAt' => '2025-01-22T10:00:00.000Z', + 'projectId' => 'proj-abc123', + 'environment' => 'production', + 'bookmarked' => false, + 'public' => false, + ], + [ + 'id' => 'session-def456', + 'createdAt' => '2025-01-22T09:00:00.000Z', + 'projectId' => 'proj-abc123', + 'environment' => 'development', + 'bookmarked' => true, + 'public' => true, + ], + ], + 'meta' => [ + 'page' => 1, + 'limit' => 10, + 'totalPages' => 1, + 'totalItems' => 2, + ], + ]; + } +} diff --git a/src/Testing/Responses/GetSessionResponse.php b/src/Testing/Responses/GetSessionResponse.php new file mode 100644 index 0000000..db4f2a9 --- /dev/null +++ b/src/Testing/Responses/GetSessionResponse.php @@ -0,0 +1,47 @@ +|string> $headers + * @param array $data + */ + public function __construct(int $status = 200, array $headers = [], string $version = '1.1', ?string $reason = null, array $data = []) + { + parent::__construct($status, $headers, (string) json_encode($this->payload($data)), $version, $reason); + } + + /** + * @param array $data + * @return array + */ + public function payload(array $data = []): array + { + return array_merge([ + 'id' => 'session-abc123', + 'createdAt' => '2025-01-22T10:00:00.000Z', + 'projectId' => 'proj-abc123', + 'environment' => 'production', + 'bookmarked' => false, + 'public' => false, + 'traces' => [ + [ + 'id' => 'trace-001', + 'name' => 'trace-in-session', + 'timestamp' => '2025-01-22T10:30:00.000Z', + ], + [ + 'id' => 'trace-002', + 'name' => 'another-trace', + 'timestamp' => '2025-01-22T10:35:00.000Z', + ], + ], + ], $data); + } +} diff --git a/src/Transporters/HttpTransporter.php b/src/Transporters/HttpTransporter.php index 319cf49..a2b734b 100644 --- a/src/Transporters/HttpTransporter.php +++ b/src/Transporters/HttpTransporter.php @@ -21,8 +21,7 @@ class HttpTransporter implements TransporterInterface { public function __construct( public readonly ClientInterface $client - ) { - } + ) {} /** * @throws BadRequestException @@ -45,7 +44,7 @@ public function request(string $method, string $uri, array $options = []): Respo } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -61,7 +60,7 @@ public function get(string $uri, array $options = []): ResponseInterface } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -82,7 +81,7 @@ public function postJson(string $uri, array $data = [], array $options = []): Re } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -98,8 +97,8 @@ public function delete(string $uri, array $options = []): ResponseInterface } /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options * * @throws BadRequestException * @throws ForbiddenException diff --git a/tests/Feature/SessionTest.php b/tests/Feature/SessionTest.php new file mode 100644 index 0000000..7b496ad --- /dev/null +++ b/tests/Feature/SessionTest.php @@ -0,0 +1,102 @@ + $handlerStack]); + + $session = (new Langfuse(new HttpTransporter($client))) + ->session() + ->get('session-abc123'); + + expect($session)->toBeInstanceOf(SessionResponse::class) + ->and($session->id)->toBe('session-abc123') + ->and($session->projectId)->toBe('proj-abc123') + ->and($session->environment)->toBe('production') + ->and($session->bookmarked)->toBe(false) + ->and($session->public)->toBe(false) + ->and($session->traces)->toBeArray() + ->and($session->traces)->toHaveCount(2); +}); + +it('can list sessions', function (): void { + $mock = new MockHandler([ + new GetSessionListResponse, + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $sessions = (new Langfuse(new HttpTransporter($client))) + ->session() + ->list(); + + expect($sessions)->toBeInstanceOf(SessionListResponse::class) + ->and($sessions->data)->toBeArray() + ->and($sessions->data)->toHaveCount(2) + ->and($sessions->data[0])->toBeInstanceOf(SessionResponse::class) + ->and($sessions->data[0]->id)->toBe('session-abc123') + ->and($sessions->data[1]->id)->toBe('session-def456') + ->and($sessions->meta)->toBeInstanceOf(MetaData::class) + ->and($sessions->meta->page)->toBe(1) + ->and($sessions->meta->totalItems)->toBe(2); +}); + +it('can list sessions with filters', function (): void { + $mock = new MockHandler([ + new GetSessionListResponse, + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $sessions = (new Langfuse(new HttpTransporter($client))) + ->session() + ->list( + page: 1, + limit: 10, + fromTimestamp: '2025-01-01T00:00:00.000Z', + toTimestamp: '2025-01-31T23:59:59.999Z', + environment: ['production'], + ); + + expect($sessions)->toBeInstanceOf(SessionListResponse::class) + ->and($sessions->data)->toBeArray(); +}); + +it('can get session with traces', function (): void { + $mock = new MockHandler([ + new GetSessionResponse(), + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $session = (new Langfuse(new HttpTransporter($client))) + ->session() + ->get('session-abc123'); + + expect($session->traces)->toBeArray() + ->and($session->traces)->not->toBeEmpty(); + + /** @var array{id: string, name: string} $trace */ + $trace = $session->traces[0]; + expect($trace['id'])->toBe('trace-001') + ->and($trace['name'])->toBe('trace-in-session'); +});