Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 368 additions & 5 deletions src/Builders/PromptBuilder.php

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions src/Providers/Models/DTO/ModelRequirements.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,28 @@ public function getRequiredCapabilities(): array
return $this->requiredCapabilities;
}

/**
* Returns requirements that also include the given capability.
*
* @since n.e.x.t
*
* @param CapabilityEnum $capability The capability to require.
* @return self The updated requirements.
*/
public function withRequiredCapability(CapabilityEnum $capability): self
{
foreach ($this->requiredCapabilities as $requiredCapability) {
if ($requiredCapability->value === $capability->value) {
return $this;
}
}

$requiredCapabilities = $this->requiredCapabilities;
$requiredCapabilities[] = $capability;

return new self($requiredCapabilities, $this->requiredOptions);
}

/**
* Gets the options that the model must support with specific values.
*
Expand Down
61 changes: 61 additions & 0 deletions src/Tools/Contracts/FunctionCallResolverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tools\Contracts;

use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;

/**
* Interface for resolving function calls requested by a model.
*
* A function call resolver executes the function calls that a model requests
* during generation and returns the results as function responses. It enables
* the automatic function call resolution loop of the
* {@see \WordPress\AiClient\Builders\PromptBuilder}: each round the resolver
* executes the requested calls, and the responses are appended to the
* conversation for a follow-up request.
*
* Resolution is split into two steps so that a round is only executed when
* every requested call can be handled:
* - {@see self::canResolve()} checks whether a call can be handled. It must be
* free of side effects, as it is invoked for every call in a round before
* any call is executed.
* - {@see self::resolve()} executes a call and returns its response. Execution
* errors should be returned as part of the function response, so the model
* can process them, rather than thrown.
*
* @since n.e.x.t
*/
interface FunctionCallResolverInterface
{
/**
* Checks whether the given function call can be resolved.
*
* This method must not have side effects. It is invoked for every function
* call in a model response before any of them is executed. If any call in
* a response cannot be resolved, none of them are executed and the
* resolution loop stops, handing the response back to the caller.
*
* @since n.e.x.t
*
* @param FunctionCall $functionCall The function call to check.
* @return bool True if the function call can be resolved.
*/
public function canResolve(FunctionCall $functionCall): bool;

/**
* Resolves the given function call by executing it.
*
* Only called for function calls that {@see self::canResolve()} reported
* as resolvable. Execution errors should be encoded in the returned
* function response, so the model can react to them.
*
* @since n.e.x.t
*
* @param FunctionCall $functionCall The function call to resolve.
* @return FunctionResponse The response for the function call.
*/
public function resolve(FunctionCall $functionCall): FunctionResponse;
}
82 changes: 82 additions & 0 deletions tests/mocks/MockFunctionCallResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

use WordPress\AiClient\Tools\Contracts\FunctionCallResolverInterface;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;

/**
* Mock function call resolver for testing.
*
* Records all checked and resolved calls. Behavior can be customized through
* optional callbacks; by default every call is resolvable and resolves to a
* simple success response.
*/
class MockFunctionCallResolver implements FunctionCallResolverInterface
{
/**
* @var callable|null Callback deciding whether a call can be resolved.
*/
private $canResolveCallback;

/**
* @var callable|null Callback producing the response for a call.
*/
private $resolveCallback;

/**
* @var list<FunctionCall> The calls passed to canResolve().
*/
public array $checkedCalls = [];

/**
* @var list<FunctionCall> The calls passed to resolve().
*/
public array $resolvedCalls = [];

/**
* @param callable|null $canResolveCallback Optional callback receiving a FunctionCall and returning a bool.
* @param callable|null $resolveCallback Optional callback receiving a FunctionCall and returning a
* FunctionResponse.
*/
public function __construct(?callable $canResolveCallback = null, ?callable $resolveCallback = null)
{
$this->canResolveCallback = $canResolveCallback;
$this->resolveCallback = $resolveCallback;
}

/**
* {@inheritDoc}
*/
public function canResolve(FunctionCall $functionCall): bool
{
$this->checkedCalls[] = $functionCall;

if ($this->canResolveCallback !== null) {
return (bool) ($this->canResolveCallback)($functionCall);
}

return true;
}

/**
* {@inheritDoc}
*/
public function resolve(FunctionCall $functionCall): FunctionResponse
{
$this->resolvedCalls[] = $functionCall;

if ($this->resolveCallback !== null) {
return ($this->resolveCallback)($functionCall);
}

return new FunctionResponse(
$functionCall->getId(),
$functionCall->getName(),
['status' => 'ok']
);
}
}
82 changes: 82 additions & 0 deletions tests/traits/MockModelCreationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace WordPress\AiClient\Tests\traits;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\DTO\ModelMessage;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
Expand Down Expand Up @@ -253,6 +254,87 @@ public function generateTextResult(array $prompt): GenerativeAiResult
};
}

/**
* Creates a mock text generation model that returns scripted results in order.
*
* Each call to generateTextResult() returns the next result from the given
* list. Once the list is exhausted, the last result is returned again.
*
* @param list<GenerativeAiResult> $results The results to return, in order. Must not be empty.
* @param ModelMetadata|null $metadata Optional metadata (uses default if not provided).
* @return ModelInterface&TextGenerationModelInterface The mock model.
*/
protected function createScriptedTextGenerationModel(
array $results,
?ModelMetadata $metadata = null
): ModelInterface {
if (empty($results)) {
throw new InvalidArgumentException('At least one scripted result must be provided.');
}

$metadata = $metadata ?? $this->createTestTextModelMetadata();

$providerMetadata = new ProviderMetadata(
'mock',
'Mock Provider',
ProviderTypeEnum::cloud()
);

return new class (
$metadata,
$providerMetadata,
$results
) implements ModelInterface, TextGenerationModelInterface {
private ModelMetadata $metadata;
private ProviderMetadata $providerMetadata;
/** @var list<GenerativeAiResult> */
private array $results;
private int $callCount = 0;
private ModelConfig $config;

/**
* @param list<GenerativeAiResult> $results
*/
public function __construct(
ModelMetadata $metadata,
ProviderMetadata $providerMetadata,
array $results
) {
$this->metadata = $metadata;
$this->providerMetadata = $providerMetadata;
$this->results = $results;
$this->config = new ModelConfig();
}

public function metadata(): ModelMetadata
{
return $this->metadata;
}

public function providerMetadata(): ProviderMetadata
{
return $this->providerMetadata;
}

public function setConfig(ModelConfig $config): void
{
$this->config = $config;
}

public function getConfig(): ModelConfig
{
return $this->config;
}

public function generateTextResult(array $prompt): GenerativeAiResult
{
$index = min($this->callCount, count($this->results) - 1);
$this->callCount++;
return $this->results[$index];
}
};
}

/**
* Creates a mock image generation model using anonymous class.
*
Expand Down
Loading
Loading