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/Comment.php b/src/Comment.php new file mode 100644 index 0000000..ecc89b1 --- /dev/null +++ b/src/Comment.php @@ -0,0 +1,115 @@ +transporter->get( + uri: sprintf('/api/public/comments/%s', urlencode($commentId)), + ); + + /** @var array{ + * id: string, + * content: string, + * objectType: string, + * objectId: string, + * authorUserId: string|null, + * createdAt: string, + * updatedAt: string, + * projectId: string + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return CommentResponse::fromArray($data); + } + + /** + * @throws JsonException + */ + public function list( + ?int $page = null, + ?int $limit = null, + ?string $objectType = null, + ?string $objectId = null, + ?string $authorUserId = null, + ): CommentListResponse { + $response = $this->transporter->get( + uri: '/api/public/comments', + options: ['query' => array_filter([ + 'page' => $page, + 'limit' => $limit, + 'objectType' => $objectType, + 'objectId' => $objectId, + 'authorUserId' => $authorUserId, + ])] + ); + + /** @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 CommentListResponse::fromArray($data); + } + + /** + * @throws JsonException + */ + public function create( + string $content, + string $objectType, + string $objectId, + ): CommentResponse { + $response = $this->transporter->postJson( + uri: '/api/public/comments', + data: [ + 'content' => $content, + 'objectType' => $objectType, + 'objectId' => $objectId, + ], + ); + + /** @var array{ + * id: string, + * content: string, + * objectType: string, + * objectId: string, + * authorUserId: string|null, + * createdAt: string, + * updatedAt: string, + * projectId: string + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return CommentResponse::fromArray($data); + } +} diff --git a/src/Concerns/IsCompilable.php b/src/Concerns/IsCompilable.php index d32399a..f6b988c 100644 --- a/src/Concerns/IsCompilable.php +++ b/src/Concerns/IsCompilable.php @@ -38,7 +38,7 @@ private function compileString(string $prompt, array $data = []): string $values = array_values($data); return str_replace( - array_map(fn ($i): string => '{{' . $i . '}}', array_keys($data)), + array_map(fn (string $i): string => '{{' . $i . '}}', array_keys($data)), $values, $prompt ); diff --git a/src/Langfuse.php b/src/Langfuse.php index ebe96f9..044404e 100644 --- a/src/Langfuse.php +++ b/src/Langfuse.php @@ -37,4 +37,11 @@ public function score(): Score transporter: $this->transporter, ); } + + public function comment(): Comment + { + return new Comment( + transporter: $this->transporter, + ); + } } diff --git a/src/Prompt.php b/src/Prompt.php index 42c6e4b..3edee52 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -21,7 +21,8 @@ class Prompt public function __construct( private readonly TransporterInterface $transporter, private readonly string $defaultLabel, - ) {} + ) { + } /** * Retrieve a text prompt by name. Uses default label if no version or label provided. @@ -49,7 +50,7 @@ public function text(string $promptName, ?int $version = null, ?string $label = /** * Retrieve a chat prompt by name. Uses default label if no version or label provided. * - * @param array|null $fallback + * @param array|null $fallback * * @throws InvalidPromptTypeException */ @@ -96,10 +97,10 @@ public function list(?string $name = null, ?int $version = null, ?string $label /** * Create a new prompt. * - * @param ($type is PromptType::TEXT ? string : array) $prompt - * @param array|null $labels - * @param array|null $config - * @param array|null $tags + * @param ($type is PromptType::TEXT ? string : array) $prompt + * @param array|null $labels + * @param array|null $config + * @param array|null $tags * @return ($type is PromptType::TEXT ? TextPromptResponse : ChatPromptResponse) * * @throws JsonException @@ -164,7 +165,7 @@ public function create(string $promptName, string|array $prompt, PromptType $typ /** * Update labels for a specific prompt version. * - * @param array $labels + * @param array $labels * * @throws JsonException */ diff --git a/src/Responses/CommentListResponse.php b/src/Responses/CommentListResponse.php new file mode 100644 index 0000000..a9ae37b --- /dev/null +++ b/src/Responses/CommentListResponse.php @@ -0,0 +1,45 @@ + $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( + CommentResponse::fromArray(...), + $data['data'] + ), + meta: MetaData::fromArray($data['meta']), + ); + } +} diff --git a/src/Responses/CommentResponse.php b/src/Responses/CommentResponse.php new file mode 100644 index 0000000..5d4c4df --- /dev/null +++ b/src/Responses/CommentResponse.php @@ -0,0 +1,46 @@ +) $prompt - */ - public function __construct( - string|array $prompt, - string $type, - ) { - parent::__construct( - prompt: $prompt, - type: $type, - ); - } - /** * Create a text fallback prompt */ diff --git a/src/Responses/PromptListResponse.php b/src/Responses/PromptListResponse.php index afd1b00..dc4e983 100644 --- a/src/Responses/PromptListResponse.php +++ b/src/Responses/PromptListResponse.php @@ -30,7 +30,7 @@ public function __construct( public static function fromArray(array $data): self { return new self( - data: array_map(fn (array $data): PromptListItem => PromptListItem::fromArray($data), $data['data']), + data: array_map(PromptListItem::fromArray(...), $data['data']), meta: MetaData::fromArray($data['meta']), pagination: PaginationData::fromArray($data['pagination']), ); diff --git a/src/Responses/ScoreListResponse.php b/src/Responses/ScoreListResponse.php index dd3c1c5..58680dc 100644 --- a/src/Responses/ScoreListResponse.php +++ b/src/Responses/ScoreListResponse.php @@ -26,7 +26,7 @@ public function __construct( public static function fromArray(array $data): self { return new self( - data: array_map(fn (array $item): ScoreResponse => ScoreResponse::fromArray($item), $data['data']), + data: array_map(ScoreResponse::fromArray(...), $data['data']), meta: MetaData::fromArray($data['meta']), ); } diff --git a/src/Testing/Responses/GetCommentListResponse.php b/src/Testing/Responses/GetCommentListResponse.php new file mode 100644 index 0000000..8ef8ebe --- /dev/null +++ b/src/Testing/Responses/GetCommentListResponse.php @@ -0,0 +1,55 @@ +|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' => 'comment-abc123', + 'content' => 'First comment', + 'objectType' => 'trace', + 'objectId' => 'trace-123', + 'authorUserId' => 'user-456', + 'createdAt' => '2025-01-22T10:00:00.000Z', + 'updatedAt' => '2025-01-22T10:00:00.000Z', + 'projectId' => 'proj-abc123', + ], + [ + 'id' => 'comment-def456', + 'content' => 'Second comment', + 'objectType' => 'observation', + 'objectId' => 'obs-789', + 'authorUserId' => null, + 'createdAt' => '2025-01-22T09:00:00.000Z', + 'updatedAt' => '2025-01-22T09:00:00.000Z', + 'projectId' => 'proj-abc123', + ], + ], + 'meta' => [ + 'page' => 1, + 'limit' => 10, + 'totalPages' => 1, + 'totalItems' => 2, + ], + ]; + } +} diff --git a/src/Testing/Responses/GetCommentResponse.php b/src/Testing/Responses/GetCommentResponse.php new file mode 100644 index 0000000..8789a95 --- /dev/null +++ b/src/Testing/Responses/GetCommentResponse.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' => 'comment-abc123', + 'content' => 'This is a test comment', + 'objectType' => 'trace', + 'objectId' => 'trace-123', + 'authorUserId' => 'user-456', + 'createdAt' => '2025-01-22T10:00:00.000Z', + 'updatedAt' => '2025-01-22T10:00:00.000Z', + 'projectId' => 'proj-abc123', + ], $data); + } +} diff --git a/src/Testing/Responses/PostCommentResponse.php b/src/Testing/Responses/PostCommentResponse.php new file mode 100644 index 0000000..403729d --- /dev/null +++ b/src/Testing/Responses/PostCommentResponse.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' => 'comment-new123', + 'content' => 'New comment content', + 'objectType' => 'trace', + 'objectId' => 'trace-123', + 'authorUserId' => 'user-456', + 'createdAt' => '2025-01-22T11:00:00.000Z', + 'updatedAt' => '2025-01-22T11:00:00.000Z', + 'projectId' => 'proj-abc123', + ], $data); + } +} diff --git a/tests/Feature/CommentTest.php b/tests/Feature/CommentTest.php new file mode 100644 index 0000000..3c42720 --- /dev/null +++ b/tests/Feature/CommentTest.php @@ -0,0 +1,106 @@ + $handlerStack]); + + $comment = (new Langfuse(new HttpTransporter($client))) + ->comment() + ->get('comment-abc123'); + + expect($comment)->toBeInstanceOf(CommentResponse::class) + ->and($comment->id)->toBe('comment-abc123') + ->and($comment->content)->toBe('This is a test comment') + ->and($comment->objectType)->toBe('trace') + ->and($comment->objectId)->toBe('trace-123') + ->and($comment->authorUserId)->toBe('user-456') + ->and($comment->projectId)->toBe('proj-abc123'); +}); + +it('can list comments', function (): void { + $mock = new MockHandler([ + new GetCommentListResponse(), + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $comments = (new Langfuse(new HttpTransporter($client))) + ->comment() + ->list(); + + expect($comments)->toBeInstanceOf(CommentListResponse::class) + ->and($comments->data)->toBeArray() + ->and($comments->data)->toHaveCount(2) + ->and($comments->data[0])->toBeInstanceOf(CommentResponse::class) + ->and($comments->data[0]->content)->toBe('First comment') + ->and($comments->data[0]->objectType)->toBe('trace') + ->and($comments->data[1]->content)->toBe('Second comment') + ->and($comments->data[1]->objectType)->toBe('observation') + ->and($comments->data[1]->authorUserId)->toBeNull() + ->and($comments->meta)->toBeInstanceOf(MetaData::class) + ->and($comments->meta->totalItems)->toBe(2); +}); + +it('can create a comment', function (): void { + $mock = new MockHandler([ + new PostCommentResponse(), + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $comment = (new Langfuse(new HttpTransporter($client))) + ->comment() + ->create( + content: 'New comment content', + objectType: 'trace', + objectId: 'trace-123', + ); + + expect($comment)->toBeInstanceOf(CommentResponse::class) + ->and($comment->id)->toBe('comment-new123') + ->and($comment->content)->toBe('New comment content') + ->and($comment->objectType)->toBe('trace') + ->and($comment->objectId)->toBe('trace-123'); +}); + +it('can list comments with filters', function (): void { + $mock = new MockHandler([ + new GetCommentListResponse(), + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $comments = (new Langfuse(new HttpTransporter($client))) + ->comment() + ->list( + page: 1, + limit: 10, + objectType: 'trace', + objectId: 'trace-123', + authorUserId: 'user-456', + ); + + expect($comments)->toBeInstanceOf(CommentListResponse::class) + ->and($comments->data)->toHaveCount(2); +}); diff --git a/tests/Feature/IngestionTest.php b/tests/Feature/IngestionTest.php index e61ca1a..61969d9 100644 --- a/tests/Feature/IngestionTest.php +++ b/tests/Feature/IngestionTest.php @@ -113,7 +113,7 @@ function getEventType(array $history, int $index = 0): string $ingestion = makeIngestion($history); // Act - $ingestion->trace(name: 'test-trace', userId: 'user-123', sessionId: 'sess-456'); + $ingestion->trace(name: 'test-trace', sessionId: 'sess-456', userId: 'user-123'); // Assert $body = getEventBody($history); @@ -166,7 +166,7 @@ function getEventType(array $history, int $index = 0): string // Act $trace = $ingestion->trace(name: 'my-trace', input: 'start'); - $result = $trace->update(output: 'final result', userId: 'user-456'); + $result = $trace->update(userId: 'user-456', output: 'final result'); // Assert expect($result)->toBe($trace) @@ -259,10 +259,10 @@ function getEventType(array $history, int $index = 0): string name: 'test-generation', input: ['messages' => [['role' => 'user', 'content' => 'Hi']]], output: 'Hello', - promptName: 'prompt-x', - promptVersion: 3, model: 'gpt-4o', modelParameters: ['temperature' => 0.2], + promptName: 'prompt-x', + promptVersion: 3, metadata: ['source' => 'test'], ); diff --git a/tests/Feature/ScoreTest.php b/tests/Feature/ScoreTest.php index aa9b510..da4ada0 100644 --- a/tests/Feature/ScoreTest.php +++ b/tests/Feature/ScoreTest.php @@ -29,9 +29,9 @@ $score = (new Langfuse(new HttpTransporter($client))) ->score() ->create( - traceId: 'trace-456', name: 'accuracy', value: 0.95, + traceId: 'trace-456', dataType: ScoreDataType::NUMERIC, ); @@ -59,9 +59,9 @@ $score = (new Langfuse(new HttpTransporter($client))) ->score() ->create( - traceId: 'trace-456', name: 'helpfulness', value: 'helpful', + traceId: 'trace-456', dataType: ScoreDataType::CATEGORICAL, ); @@ -86,9 +86,9 @@ $score = (new Langfuse(new HttpTransporter($client))) ->score() ->create( - traceId: 'trace-456', name: 'is_correct', value: 1, + traceId: 'trace-456', dataType: ScoreDataType::BOOLEAN, ); @@ -154,8 +154,8 @@ page: 1, limit: 10, name: 'accuracy', - dataType: ScoreDataType::NUMERIC, traceId: 'trace-123', + dataType: ScoreDataType::NUMERIC, ); expect($history)->toHaveCount(1); @@ -217,9 +217,9 @@ (new Langfuse(new HttpTransporter($client))) ->score() ->create( - traceId: 'trace-456', name: 'accuracy', value: 0.95, + traceId: 'trace-456', dataType: ScoreDataType::NUMERIC, id: 'custom-score-id', observationId: 'obs-789',