Skip to content
76 changes: 60 additions & 16 deletions src/Responses/Responses/Output/OutputComputerToolCall.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
* @phpstan-import-type WaitType from Wait
* @phpstan-import-type PendingSafetyCheckType from OutputComputerPendingSafetyCheck
*
* @phpstan-type OutputComputerToolCallType array{action: ClickType|DoubleClickType|DragType|KeyPressType|MoveType|ScreenshotType|ScrollType|TypeType|WaitType, call_id: string, id: string, pending_safety_checks: array<int, PendingSafetyCheckType>, status: 'in_progress'|'completed'|'incomplete', type: 'computer_call'}
* @phpstan-type ActionType ClickType|DoubleClickType|DragType|KeyPressType|MoveType|ScreenshotType|ScrollType|TypeType|WaitType
* @phpstan-type OutputComputerToolCallType array{action?: ActionType, actions?: array<int, ActionType>, call_id: string, id: string, pending_safety_checks?: array<int, PendingSafetyCheckType>, status: 'in_progress'|'completed'|'incomplete', type: 'computer_call'}
*
* @implements ResponseContract<OutputComputerToolCallType>
*/
Expand All @@ -44,12 +45,13 @@ final class OutputComputerToolCall implements ResponseContract
use Fakeable;

/**
* @param array<int, Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait> $actions
* @param array<int, OutputComputerPendingSafetyCheck> $pendingSafetyChecks
* @param 'in_progress'|'completed'|'incomplete' $status
* @param 'computer_call' $type
*/
private function __construct(
public readonly Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait $action,
public readonly array $actions,
public readonly string $callId,
public readonly string $id,
public readonly array $pendingSafetyChecks,
Expand All @@ -62,25 +64,26 @@ private function __construct(
*/
public static function from(array $attributes): self
{
$action = match ($attributes['action']['type']) {
'click' => Click::from($attributes['action']),
'double_click' => DoubleClick::from($attributes['action']),
'drag' => Drag::from($attributes['action']),
'keypress' => KeyPress::from($attributes['action']),
'move' => Move::from($attributes['action']),
'screenshot' => Screenshot::from($attributes['action']),
'scroll' => Scroll::from($attributes['action']),
'type' => Type::from($attributes['action']),
'wait' => Wait::from($attributes['action']),
};
/** @var array<int, ActionType> $actionAttributes */
$actionAttributes = [];
if (isset($attributes['actions'])) {
$actionAttributes = $attributes['actions'];
} elseif (isset($attributes['action'])) {
$actionAttributes = [$attributes['action']];
}

$actions = array_map(
static fn (array $action): Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait => self::mapAction($action),
$actionAttributes
);

$pendingSafetyChecks = array_map(
fn (array $safetyCheck): OutputComputerPendingSafetyCheck => OutputComputerPendingSafetyCheck::from($safetyCheck),
$attributes['pending_safety_checks']
$attributes['pending_safety_checks'] ?? []
);

return new self(
action: $action,
actions: $actions,
callId: $attributes['call_id'],
id: $attributes['id'],
pendingSafetyChecks: $pendingSafetyChecks,
Expand All @@ -98,12 +101,53 @@ public function toArray(): array
'type' => $this->type,
'call_id' => $this->callId,
'id' => $this->id,
'action' => $this->action->toArray(),
'actions' => array_map(
fn (Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait $action): array => $action->toArray(),
$this->actions,
),
'pending_safety_checks' => array_map(
fn (OutputComputerPendingSafetyCheck $safetyCheck): array => $safetyCheck->toArray(),
$this->pendingSafetyChecks,
),
'status' => $this->status,
];
}

/**
* @param array<string, mixed> $action
*/
private static function mapAction(array $action): Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems unneeded. I'll take it from here. The AI got it like 90% right

{
switch ($action['type'] ?? null) {
case 'click':
/** @var ClickType $action */
return Click::from($action);
case 'double_click':
/** @var DoubleClickType $action */
return DoubleClick::from($action);
case 'drag':
/** @var DragType $action */
return Drag::from($action);
case 'keypress':
/** @var KeyPressType $action */
return KeyPress::from($action);
case 'move':
/** @var MoveType $action */
return Move::from($action);
case 'screenshot':
/** @var ScreenshotType $action */
return Screenshot::from($action);
case 'scroll':
/** @var ScrollType $action */
return Scroll::from($action);
case 'type':
/** @var TypeType $action */
return Type::from($action);
case 'wait':
/** @var WaitType $action */
return Wait::from($action);
default:
throw new \InvalidArgumentException('Invalid or missing action type in computer action payload.');
}
}
}
12 changes: 7 additions & 5 deletions tests/Fixtures/Responses.php
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,13 @@ function outputComputerToolCall(): array
'type' => 'computer_call',
'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c',
'id' => 'cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c',
'action' => [
'button' => 'left',
'type' => 'click',
'x' => 117,
'y' => 123,
'actions' => [
[
'button' => 'left',
'type' => 'click',
'x' => 117,
'y' => 123,
],
],
'pending_safety_checks' => [
[
Expand Down
95 changes: 94 additions & 1 deletion tests/Responses/Responses/Output/OutputComputerToolCall.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
<?php

use OpenAI\Responses\Responses\Output\ComputerAction\OutputComputerActionClick;
use OpenAI\Responses\Responses\Output\ComputerAction\OutputComputerActionScreenshot;
use OpenAI\Responses\Responses\Output\ComputerAction\OutputComputerActionWait;
use OpenAI\Responses\Responses\Output\OutputComputerToolCall;

test('from', function () {
$response = OutputComputerToolCall::from(outputComputerToolCall());

expect($response)
->toBeInstanceOf(OutputComputerToolCall::class)
->action->toBeInstanceOf(OutputComputerActionClick::class)
->actions->toBeArray()->toHaveCount(1);

expect($response->actions[0])
->toBeInstanceOf(OutputComputerActionClick::class);

expect($response)
->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c')
->id->toBe('cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c')
->status->toBe('completed')
Expand All @@ -28,3 +35,89 @@
->toBeArray()
->toBe(outputComputerToolCall());
});

test('from with actions and without pending safety checks', function () {
$payload = outputComputerToolCall();
unset($payload['pending_safety_checks']);
$payload['actions'] = [
['type' => 'screenshot'],
];

$response = OutputComputerToolCall::from($payload);

expect($response)
->toBeInstanceOf(OutputComputerToolCall::class)
->actions->toBeArray()->toHaveCount(1)
->pendingSafetyChecks->toBeArray()->toHaveCount(0)
->status->toBe('completed')
->type->toBe('computer_call');

expect($response->actions[0])->toBeInstanceOf(OutputComputerActionScreenshot::class);

expect($response->toArray())
->toBeArray()
->toMatchArray([
'actions' => [['type' => 'screenshot']],
'pending_safety_checks' => [],
]);
});

test('from with multiple actions maps all actions', function () {
$payload = outputComputerToolCall();
$payload['actions'] = [
[
'button' => 'left',
'type' => 'click',
'x' => 117,
'y' => 123,
],
['type' => 'wait'],
];

$response = OutputComputerToolCall::from($payload);

expect($response->actions)
->toHaveCount(2);

expect($response->actions[0])->toBeInstanceOf(OutputComputerActionClick::class);
expect($response->actions[1])->toBeInstanceOf(OutputComputerActionWait::class);
});

test('from with empty actions returns empty actions array', function () {
$payload = outputComputerToolCall();
$payload['actions'] = [];

$response = OutputComputerToolCall::from($payload);

expect($response->actions)->toBeArray()->toHaveCount(0);
expect($response->toArray()['actions'])->toBeArray()->toHaveCount(0);
});

test('from without action and actions keys returns empty actions array', function () {
$payload = outputComputerToolCall();
unset($payload['action'], $payload['actions']);

$response = OutputComputerToolCall::from($payload);

expect($response->actions)->toBeArray()->toHaveCount(0);
expect($response->toArray()['actions'])->toBeArray()->toHaveCount(0);
});

test('from with malformed actions payload throws exception', function () {
$payload = outputComputerToolCall();
$payload['actions'] = [
['x' => 1],
];

expect(fn (): OutputComputerToolCall => OutputComputerToolCall::from($payload))
->toThrow(InvalidArgumentException::class, 'Invalid or missing action type in computer action payload.');
});

test('from with malformed legacy action payload throws exception', function () {
$payload = outputComputerToolCall();
unset($payload['actions']);
$payload['action'] = ['x' => 1];

expect(fn (): OutputComputerToolCall => OutputComputerToolCall::from($payload))
->toThrow(InvalidArgumentException::class, 'Invalid or missing action type in computer action payload.');
});