Skip to content
Merged
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
30 changes: 30 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,33 @@ jobs:

- name: Run phpstan
run: vendor/bin/phpstan analyse -c phpstan.dist.neon --no-progress --error-format=github

type-coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo

- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-

- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress

- name: Check type coverage
run: vendor/bin/pest --type-coverage --min=100
85 changes: 80 additions & 5 deletions src/LLM/AbstractLLM.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
use Cortex\Contracts\OutputParser;
use Cortex\Support\Traits\CanPipe;
use Cortex\Exceptions\LLMException;
use Cortex\LLM\Data\ChatGeneration;
use Cortex\JsonSchema\SchemaFactory;
use Cortex\ModelInfo\Data\ModelInfo;
use Cortex\LLM\Data\ChatStreamResult;
use Cortex\Exceptions\PipelineException;
use Cortex\LLM\Data\ChatGenerationChunk;
use Cortex\ModelInfo\Enums\ModelFeature;
use Cortex\JsonSchema\Types\ObjectSchema;
use Cortex\LLM\Data\Messages\UserMessage;
Expand Down Expand Up @@ -50,6 +52,8 @@ abstract class AbstractLLM implements LLM

protected ?StructuredOutputConfig $structuredOutputConfig = null;

protected StructuredOutputMode $structuredOutputMode = StructuredOutputMode::Auto;

protected ?OutputParser $outputParser = null;

protected ?string $outputParserError = null;
Expand All @@ -64,6 +68,8 @@ abstract class AbstractLLM implements LLM

protected bool $shouldApplyFormatInstructions = false;

protected bool $shouldParseOutput = true;

/**
* @var array<\Cortex\ModelInfo\Enums\ModelFeature>
*/
Expand Down Expand Up @@ -116,15 +122,19 @@ public function output(OutputParser $parser): Pipeline
/**
* @param array<int, \Cortex\LLM\Contracts\Tool|\Cortex\JsonSchema\Contracts\Schema|\Closure|string> $tools
*/
public function withTools(array $tools, ToolChoice|string $toolChoice = ToolChoice::Auto): static
{
public function withTools(
array $tools,
ToolChoice|string $toolChoice = ToolChoice::Auto,
bool $allowParallelToolCalls = true,
): static {
$this->supportsFeatureOrFail(ModelFeature::ToolCalling);

$this->toolConfig = $tools === []
? null
: new ToolConfig(
Utils::toToolCollection($tools)->all(),
$toolChoice,
$allowParallelToolCalls,
);

return $this;
Expand All @@ -133,9 +143,15 @@ public function withTools(array $tools, ToolChoice|string $toolChoice = ToolChoi
/**
* Add a tool to the LLM.
*/
public function addTool(Tool|Closure|string $tool, ToolChoice|string $toolChoice = ToolChoice::Auto): static
{
return $this->withTools([...($this->toolConfig->tools ?? []), $tool], $toolChoice);
public function addTool(
Tool|Closure|string $tool,
ToolChoice|string $toolChoice = ToolChoice::Auto,
bool $allowParallelToolCalls = true,
): static {
return $this->withTools([
...($this->toolConfig->tools ?? []),
$tool,
], $toolChoice, $allowParallelToolCalls);
}

/**
Expand Down Expand Up @@ -166,6 +182,7 @@ public function withStructuredOutput(
bool $strict = true,
StructuredOutputMode $outputMode = StructuredOutputMode::Auto,
): static {
$this->structuredOutputMode = $outputMode;
[$schema, $outputParser] = $this->resolveSchemaAndOutputParser($output, $strict);

$this->withOutputParser($outputParser);
Expand Down Expand Up @@ -386,6 +403,64 @@ protected static function applyFormatInstructions(
return $messages;
}

public function shouldParseOutput(bool $shouldParseOutput = true): static
{
$this->shouldParseOutput = $shouldParseOutput;

return $this;
}

protected function applyOutputParserIfApplicable(
ChatGeneration|ChatGenerationChunk $generationOrChunk,
): ChatGeneration|ChatGenerationChunk {
if ($this->shouldParseOutput && $this->outputParser !== null) {
try {
$parsedOutput = $this->outputParser->parse($generationOrChunk);
$generationOrChunk = $generationOrChunk->cloneWithParsedOutput($parsedOutput);
} catch (OutputParserException $e) {
$this->outputParserError = $e->getMessage();
}
}

return $generationOrChunk;
}

/**
* Apply the format instructions to the messages if applicable.
*
* @return \Cortex\LLM\Data\Messages\MessageCollection<int, \Cortex\LLM\Contracts\Message>
*/
protected function applyFormatInstructionsIfApplicable(
MessageCollection $messages,
): MessageCollection {
if ($this->shouldApplyFormatInstructions && $formatInstructions = $this->outputParser?->formatInstructions()) {
return static::applyFormatInstructions($messages, $formatInstructions);
}

return $messages;
}

/**
* Resolve the messages to be used for the LLM.
*
* @param \Cortex\LLM\Data\Messages\MessageCollection|\Cortex\LLM\Contracts\Message|non-empty-array<int, \Cortex\LLM\Contracts\Message|\Cortex\LLM\Data\Messages\MessagePlaceholder>|string $messages
*
* @throws \Cortex\Exceptions\LLMException If no messages are provided
*
* @return \Cortex\LLM\Data\Messages\MessageCollection<int, \Cortex\LLM\Contracts\Message>
*/
protected function resolveMessages(
MessageCollection|Message|array|string $messages,
): MessageCollection {
$messages = Utils::toMessageCollection($messages)->withoutPlaceholders();

if ($messages->isEmpty()) {
throw new LLMException('You must provide at least one message to the LLM.');
}

return $this->applyFormatInstructionsIfApplicable($messages);
}

/**
* Resolve the schema and output parser from the given output type.
*
Expand Down
7 changes: 7 additions & 0 deletions src/LLM/CacheDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ public function ignoreFeatures(bool $ignoreModelFeatures = true): static
return $this;
}

public function shouldParseOutput(bool $shouldParseOutput = true): static
{
$this->llm = $this->llm->shouldParseOutput($shouldParseOutput);

return $this;
}

public function withModel(string $model): static
{
$this->llm = $this->llm->withModel($model);
Expand Down
7 changes: 7 additions & 0 deletions src/LLM/Contracts/LLM.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,11 @@ public function getFeatures(): array;
* Get the model info for the LLM.
*/
public function getModelInfo(): ?ModelInfo;

/**
* Set whether the output should be parsed.
* This may be set to false when called in a pipeline context and output parsing
* is done as part of the next pipeable.
*/
public function shouldParseOutput(bool $shouldParseOutput = true): static;
}
4 changes: 2 additions & 2 deletions src/LLM/Contracts/Tool.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Cortex\LLM\Contracts;

use Cortex\LLM\Data\ToolCall;
use Cortex\JsonSchema\Contracts\Schema;
use Cortex\JsonSchema\Types\ObjectSchema;
use Cortex\LLM\Data\Messages\ToolMessage;

interface Tool
Expand All @@ -23,7 +23,7 @@ public function description(): string;
/**
* Get the schema of the tool.
*/
public function schema(): Schema;
public function schema(): ObjectSchema;

/**
* Get the formatted output of the tool.
Expand Down
1 change: 1 addition & 0 deletions src/LLM/Data/Messages/AssistantMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public ?ToolCallCollection $toolCalls = null,
public ?string $name = null,
public ?ResponseMetadata $metadata = null,
public ?string $id = null,
) {
$this->role = MessageRole::Assistant;
}
Expand Down
1 change: 1 addition & 0 deletions src/LLM/Data/Messages/Content/ReasoningContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
readonly class ReasoningContent extends AbstractContent
{
public function __construct(
public string $id,
public string $reasoning,
) {}
}
6 changes: 3 additions & 3 deletions src/LLM/Data/Messages/Content/TextContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
readonly class TextContent extends AbstractContent
{
public function __construct(
public string $text,
public ?string $text = null,
) {}

#[Override]
public function variables(): array
{
return Utils::findVariables($this->text);
return Utils::findVariables($this->text ?? '');
}

#[Override]
public function replaceVariables(array $variables): self
{
return new self(Utils::replaceVariables($this->text, $variables));
return new self(Utils::replaceVariables($this->text ?? '', $variables));
}
}
1 change: 1 addition & 0 deletions src/LLM/Data/ToolConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
public function __construct(
public array $tools,
public ToolChoice|string $toolChoice = ToolChoice::Auto,
public bool $allowParallelToolCalls = true,
) {}
}
1 change: 1 addition & 0 deletions src/LLM/Data/Usage.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(
public int $promptTokens,
public ?int $completionTokens = null,
public ?int $cachedTokens = null,
public ?int $reasoningTokens = null,
?int $totalTokens = null,
public ?float $inputCost = null,
public ?float $outputCost = null,
Expand Down
Loading
Loading