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/src/DatasetRunItem.php b/src/DatasetRunItem.php new file mode 100644 index 0000000..ceb3b3c --- /dev/null +++ b/src/DatasetRunItem.php @@ -0,0 +1,71 @@ +|null $metadata + * + * @throws JsonException + */ + public function create( + string $runName, + string $datasetItemId, + ?string $traceId = null, + ?string $observationId = null, + ?string $runDescription = null, + ?array $metadata = null, + ): DatasetRunItemResponse { + $response = $this->transporter->postJson( + '/api/public/dataset-run-items', + array_filter([ + 'runName' => $runName, + 'datasetItemId' => $datasetItemId, + 'traceId' => $traceId, + 'observationId' => $observationId, + 'runDescription' => $runDescription, + 'metadata' => $metadata, + ], fn ($value) => $value !== null) + ); + + /** @var array{id: string, datasetRunId: string, datasetRunName: string, datasetItemId: string, traceId: string, createdAt: string, updatedAt: string, observationId?: string|null} $data */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return DatasetRunItemResponse::fromArray($data); + } + + /** + * @throws JsonException + */ + public function list( + string $datasetId, + string $runName, + ?int $page = null, + ?int $limit = null, + ): DatasetRunItemListResponse { + $response = $this->transporter->get( + '/api/public/dataset-run-items', + ['query' => array_filter([ + 'datasetId' => $datasetId, + 'runName' => $runName, + 'page' => $page, + 'limit' => $limit, + ], fn ($value) => $value !== null)] + ); + + /** @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 DatasetRunItemListResponse::fromArray($data); + } +} diff --git a/src/Langfuse.php b/src/Langfuse.php index ebe96f9..8981ba4 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 datasetRunItem(): DatasetRunItem + { + return new DatasetRunItem( + transporter: $this->transporter, + ); + } } diff --git a/src/Responses/DatasetRunItemListResponse.php b/src/Responses/DatasetRunItemListResponse.php new file mode 100644 index 0000000..42de839 --- /dev/null +++ b/src/Responses/DatasetRunItemListResponse.php @@ -0,0 +1,35 @@ + $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): DatasetRunItemResponse => DatasetRunItemResponse::fromArray($item), + $data['data'] + ), + meta: MetaData::fromArray($data['meta']), + ); + } +} diff --git a/src/Responses/DatasetRunItemResponse.php b/src/Responses/DatasetRunItemResponse.php new file mode 100644 index 0000000..ee0dc48 --- /dev/null +++ b/src/Responses/DatasetRunItemResponse.php @@ -0,0 +1,45 @@ +|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([ + 'data' => [ + [ + 'id' => 'run-item-123', + 'datasetRunId' => 'run-456', + 'datasetRunName' => 'test-run', + 'datasetItemId' => 'item-789', + 'traceId' => 'trace-abc', + 'createdAt' => '2025-01-15T10:00:00.000Z', + 'updatedAt' => '2025-01-15T10:00:00.000Z', + 'observationId' => null, + ], + [ + 'id' => 'run-item-456', + 'datasetRunId' => 'run-456', + 'datasetRunName' => 'test-run', + 'datasetItemId' => 'item-012', + 'traceId' => 'trace-def', + 'createdAt' => '2025-01-16T10:00:00.000Z', + 'updatedAt' => '2025-01-16T10:00:00.000Z', + 'observationId' => 'obs-xyz', + ], + ], + 'meta' => [ + 'page' => 1, + 'limit' => 50, + 'totalPages' => 1, + 'totalItems' => 2, + ], + ], $data); + } +} diff --git a/src/Testing/Responses/GetDatasetRunItemResponse.php b/src/Testing/Responses/GetDatasetRunItemResponse.php new file mode 100644 index 0000000..0786777 --- /dev/null +++ b/src/Testing/Responses/GetDatasetRunItemResponse.php @@ -0,0 +1,37 @@ +|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' => 'run-item-123', + 'datasetRunId' => 'run-456', + 'datasetRunName' => 'test-run', + 'datasetItemId' => 'item-789', + 'traceId' => 'trace-abc', + 'createdAt' => '2025-01-15T10:00:00.000Z', + 'updatedAt' => '2025-01-15T10:00:00.000Z', + 'observationId' => null, + ], $data); + } +} diff --git a/tests/Feature/DatasetRunItemTest.php b/tests/Feature/DatasetRunItemTest.php new file mode 100644 index 0000000..e1cc41e --- /dev/null +++ b/tests/Feature/DatasetRunItemTest.php @@ -0,0 +1,148 @@ + $handlerStack]); + + $item = (new Langfuse(new HttpTransporter($client))) + ->datasetRunItem() + ->create( + runName: 'test-run', + datasetItemId: 'item-789', + traceId: 'trace-abc', + ); + + expect($item)->toBeInstanceOf(DatasetRunItemResponse::class) + ->and($item->id)->toBe('run-item-123') + ->and($item->datasetRunId)->toBe('run-456') + ->and($item->datasetRunName)->toBe('test-run') + ->and($item->datasetItemId)->toBe('item-789') + ->and($item->traceId)->toBe('trace-abc'); +}); + +it('can list dataset run items', function (): void { + $mock = new MockHandler([ + new GetDatasetRunItemListResponse, + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $items = (new Langfuse(new HttpTransporter($client))) + ->datasetRunItem() + ->list( + datasetId: 'dataset-123', + runName: 'test-run', + ); + + expect($items)->toBeInstanceOf(DatasetRunItemListResponse::class) + ->and($items->data)->toBeArray() + ->and($items->data)->toHaveCount(2) + ->and($items->data[0])->toBeInstanceOf(DatasetRunItemResponse::class) + ->and($items->data[0]->id)->toBe('run-item-123') + ->and($items->data[1]->id)->toBe('run-item-456') + ->and($items->data[1]->observationId)->toBe('obs-xyz') + ->and($items->meta)->toBeInstanceOf(MetaData::class) + ->and($items->meta->totalItems)->toBe(2); +}); + +it('can list dataset run items with pagination', function (): void { + /** @var array $history */ + $history = []; + + $mock = new MockHandler([ + new GetDatasetRunItemListResponse, + ]); + + $stack = HandlerStack::create($mock); + $stack->push(Middleware::history($history)); + + $client = new Client(['handler' => $stack]); + + (new Langfuse(new HttpTransporter($client))) + ->datasetRunItem() + ->list( + datasetId: 'dataset-123', + runName: 'test-run', + page: 2, + limit: 10, + ); + + expect($history)->toHaveCount(1); + + /** @var array{request: RequestInterface} $first */ + $first = $history[0]; + /** @var RequestInterface $request */ + $request = $first['request']; + $query = $request->getUri()->getQuery(); + + expect($query)->toContain('datasetId=dataset-123') + ->and($query)->toContain('runName=test-run') + ->and($query)->toContain('page=2') + ->and($query)->toContain('limit=10'); +}); + +it('sends correct payload when creating a dataset run item', function (): void { + /** @var array $history */ + $history = []; + + $mock = new MockHandler([ + new GetDatasetRunItemResponse, + ]); + + $stack = HandlerStack::create($mock); + $stack->push(Middleware::history($history)); + + $client = new Client(['handler' => $stack]); + + (new Langfuse(new HttpTransporter($client))) + ->datasetRunItem() + ->create( + runName: 'my-run', + datasetItemId: 'item-abc', + traceId: 'trace-123', + observationId: 'obs-456', + runDescription: 'Test run description', + metadata: ['env' => 'test'], + ); + + expect($history)->toHaveCount(1); + + /** @var array{request: RequestInterface} $first */ + $first = $history[0]; + /** @var RequestInterface $request */ + $request = $first['request']; + + expect($request->getMethod())->toBe('POST') + ->and((string) $request->getUri())->toContain('/api/public/dataset-run-items') + ->and($request->getHeaderLine('Content-Type'))->toContain('application/json'); + + /** @var array $body */ + $body = json_decode((string) $request->getBody(), true); + + expect($body['runName'])->toBe('my-run') + ->and($body['datasetItemId'])->toBe('item-abc') + ->and($body['traceId'])->toBe('trace-123') + ->and($body['observationId'])->toBe('obs-456') + ->and($body['runDescription'])->toBe('Test run description') + ->and($body['metadata'])->toBe(['env' => 'test']); +});