Skip to content

Commit a7c48fa

Browse files
committed
feat: introduce dedicated exceptions
1 parent 29b3ad8 commit a7c48fa

22 files changed

Lines changed: 334 additions & 73 deletions

app/Services/Ai/Agents/Adapters/AbstractLaravelAgent.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
use App\Services\Ai\Agents\Contracts\AgentInterface as HawkiAgentInterface;
9+
use App\Services\Ai\Agents\Exceptions\AgentStateException;
910
use App\Services\Ai\LaravelAi\Values\ProviderDriverPortal;
1011
use App\Services\Ai\Values\TokenUsage;
1112
use Laravel\Ai\Contracts\Agent as LaravelAgentInterface;
@@ -30,8 +31,7 @@ protected function getAttachments(): array
3031
public function getUsage(): TokenUsage
3132
{
3233
if (!$this->usage) {
33-
// @todo exception
34-
throw new \RuntimeException('Usage is not available. Please call send() or sendStreaming() first.');
34+
throw AgentStateException::forUsageNotAvailable();
3535
}
3636

3737
return TokenUsage::fromLaravelUsage($this->usage, $this->getContext()->model);

app/Services/Ai/Agents/Adapters/AbstractTextGeneratingAgent.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Services\Ai\Agents\Adapters;
66

77

8+
use App\Services\Ai\Agents\Exceptions\InvalidAgentConfigurationException;
89
use App\Services\Ai\Agents\Middleware\LoggingMiddleware;
910
use App\Services\Ai\Agents\Utils\MessageMetaBlocks;
1011
use App\Services\Ai\Agents\Values\AgentRequestContext;
@@ -31,22 +32,18 @@ public function __construct(
3132
{
3233
if (empty($this->promptString)) {
3334
if (empty($this->messages)) {
34-
// @todo exception
35-
throw new \InvalidArgumentException('Either promptString or messages must be provided.');
35+
throw InvalidAgentConfigurationException::forMissingPromptOrMessages();
3636
}
3737

3838
$lastMessage = array_pop($this->messages);
3939
if (!$lastMessage instanceof Message) {
40-
// @todo exception
41-
throw new \InvalidArgumentException('The last message must be an instance of Laravel\Ai\Messages\Message.');
40+
throw InvalidAgentConfigurationException::forLastMessageNotAMessageInstance();
4241
}
4342
if ($lastMessage->role !== MessageRole::User) {
44-
// @todo exception
45-
throw new \InvalidArgumentException('The last message must have the role of user if the promptString is not provided.');
43+
throw InvalidAgentConfigurationException::forLastMessageNotUserRole();
4644
}
4745
if (empty($lastMessage->content)) {
48-
// @todo exception
49-
throw new \InvalidArgumentException('The last message must have content if the promptString is not provided.');
46+
throw InvalidAgentConfigurationException::forLastMessageEmptyContent();
5047
}
5148
$this->promptString = $lastMessage->content;
5249
if (empty($this->attachments) && $lastMessage instanceof UserMessage) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace App\Services\Ai\Agents\Exceptions;
5+
6+
class AgentStateException extends \RuntimeException implements AgentExceptionInterface
7+
{
8+
public static function forUsageNotAvailable(): self
9+
{
10+
return new self('Token usage is not available. Call send() or sendStreaming() before reading usage.');
11+
}
12+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace App\Services\Ai\Agents\Exceptions;
5+
6+
use Laravel\Ai\Messages\Message;
7+
8+
class InvalidAgentConfigurationException extends \InvalidArgumentException implements AgentExceptionInterface
9+
{
10+
public static function forMissingPromptOrMessages(): self
11+
{
12+
return new self('Either a promptString or a non-empty messages array must be provided to the agent.');
13+
}
14+
15+
public static function forLastMessageNotAMessageInstance(): self
16+
{
17+
return new self(sprintf(
18+
'The last entry in the messages array must be an instance of %s.',
19+
Message::class
20+
));
21+
}
22+
23+
public static function forLastMessageNotUserRole(): self
24+
{
25+
return new self('The last message must have the role of "user" when no promptString is provided.');
26+
}
27+
28+
public static function forLastMessageEmptyContent(): self
29+
{
30+
return new self('The last message must have non-empty content when no promptString is provided.');
31+
}
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace App\Services\Ai\Agents\Exceptions;
5+
6+
use Laravel\Ai\Messages\MessageRole;
7+
8+
class InvalidLegacyRequestPayloadException extends \InvalidArgumentException implements AgentExceptionInterface
9+
{
10+
public static function forMissingSystemInstructions(): self
11+
{
12+
return new self('No system instructions found in messages payload.');
13+
}
14+
15+
public static function forMessageMissingFields(): self
16+
{
17+
return new self('Each message must have a "role" and "content.text" field.');
18+
}
19+
20+
public static function forInvalidMessageRole(string $role): self
21+
{
22+
return new self(sprintf(
23+
'Invalid message role "%s". Allowed roles are "%s" and "%s".',
24+
$role,
25+
MessageRole::User->value,
26+
MessageRole::Assistant->value
27+
));
28+
}
29+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace App\Services\Ai\Agents\Exceptions;
5+
6+
class InvalidToolTransferStringException extends \InvalidArgumentException implements AgentExceptionInterface
7+
{
8+
public static function forNotAString(): self
9+
{
10+
return new self('Tool transfer strings must be an array of strings.');
11+
}
12+
13+
public static function forInvalidType(string $transferString): self
14+
{
15+
return new self(sprintf(
16+
'Tool transfer string "%s" must describe either a capability or a tool name.',
17+
$transferString
18+
));
19+
}
20+
21+
public static function forCapabilityNotFound(string $capabilityKey): self
22+
{
23+
return new self(sprintf('Capability "%s" is not registered.', $capabilityKey));
24+
}
25+
26+
public static function forCapabilityMissingInnerTool(string $capabilityKey): self
27+
{
28+
return new self(sprintf(
29+
'Capability "%s" requires an inner tool to be specified in the transfer string.',
30+
$capabilityKey
31+
));
32+
}
33+
34+
public static function forSettingsNotJsonObject(string $settingsString): self
35+
{
36+
return new self(sprintf(
37+
'Settings in tool transfer string must be a JSON object, got: "%s".',
38+
$settingsString
39+
));
40+
}
41+
42+
public static function forInvalidJsonSettings(string $settingsString, \JsonException $previous): self
43+
{
44+
return new self(
45+
sprintf('Invalid JSON settings in tool transfer string: "%s".', $settingsString),
46+
0,
47+
$previous
48+
);
49+
}
50+
51+
public static function forMissingCapabilityOrToolName(string $transferString): self
52+
{
53+
return new self(sprintf(
54+
'Tool transfer string "%s" is missing the capability name or inner tool name.',
55+
$transferString
56+
));
57+
}
58+
59+
public static function forEmptyToolName(string $transferString): self
60+
{
61+
return new self(sprintf(
62+
'Tool transfer string "%s" does not contain a tool name.',
63+
$transferString
64+
));
65+
}
66+
}

app/Services/Ai/Agents/Implementations/Chat/ChatAgentFromLegacyRequestFactory.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use App\Models\Ai\AiModel;
99
use App\Services\Ai\Agents\Contracts\AgentInterface;
10+
use App\Services\Ai\Agents\Exceptions\InvalidLegacyRequestPayloadException;
1011
use App\Services\Ai\Agents\Implementations\AbstractAgentFactory;
1112
use App\Services\Ai\Agents\Utils\AlternatingMessageHistory;
1213
use App\Services\Ai\Agents\Utils\UserMessageAttachments;
@@ -104,8 +105,7 @@ private function getInstructionsFromPayload(array $payload): string
104105
}
105106
}
106107

107-
// @todo exception
108-
throw new \RuntimeException('No system instructions found in messages payload.');
108+
throw InvalidLegacyRequestPayloadException::forMissingSystemInstructions();
109109
}
110110

111111
private function getMessagesFromPayload(array $payload, AgentRequestContext $context): array
@@ -119,19 +119,12 @@ private function getMessagesFromPayload(array $payload, AgentRequestContext $con
119119
}
120120

121121
if (!isset($payloadMessage['role'], $payloadMessage['content']['text'])) {
122-
// @todo exception
123-
throw new \InvalidArgumentException('Each message must have a "role" and "content.text" field.');
122+
throw InvalidLegacyRequestPayloadException::forMessageMissingFields();
124123
}
125124

126125
$payloadRole = MessageRole::tryFrom($payloadMessage['role']);
127126
if (!in_array($payloadRole, [MessageRole::User, MessageRole::Assistant], true)) {
128-
// @todo exception
129-
throw new \InvalidArgumentException(sprintf(
130-
'Invalid message role "%s". Allowed roles are "%s" and "%s".',
131-
$payloadRole->value ?? $payloadMessage['role'],
132-
MessageRole::User->value,
133-
MessageRole::Assistant->value
134-
));
127+
throw InvalidLegacyRequestPayloadException::forInvalidMessageRole($payloadRole->value ?? $payloadMessage['role']);
135128
}
136129

137130
if ($payloadRole === MessageRole::User) {

app/Services/Ai/Agents/Implementations/Chat/ChatToolResolver.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Services\Ai\Agents\Implementations\Chat;
66

77

8+
use App\Services\Ai\Agents\Exceptions\InvalidToolTransferStringException;
89
use App\Services\Ai\Agents\Implementations\Chat\Values\ToolTransferData;
910
use App\Services\Ai\Agents\Values\AgentRequestContext;
1011
use App\Services\Ai\Models\Capabilities\AiModelCapabilityRegistry;
@@ -28,8 +29,7 @@ public function findTools(
2829
{
2930
foreach ($toolTransferStrings as $toolTransferString) {
3031
if (!is_string($toolTransferString)) {
31-
// @todo exception
32-
throw new \InvalidArgumentException('Tool transfer strings must be an array of strings.');
32+
throw InvalidToolTransferStringException::forNotAString();
3333
}
3434

3535
$toolData = ToolTransferData::fromString($toolTransferString);
@@ -44,8 +44,7 @@ public function findTools(
4444
continue;
4545
}
4646

47-
// @todo exception
48-
throw new \InvalidArgumentException('Tool transfer data must be either a capability or a tool name.');
47+
throw InvalidToolTransferStringException::forInvalidType($toolTransferString);
4948
}
5049
}
5150

@@ -56,13 +55,11 @@ private function findToolByCapability(
5655
{
5756
$capability = $this->capabilityRegistry->getDefinition($toolData->toolOrCapability);
5857
if (!$capability) {
59-
// @todo exception
60-
throw new \InvalidArgumentException('Capability not found: ' . $toolData->toolOrCapability);
58+
throw InvalidToolTransferStringException::forCapabilityNotFound($toolData->toolOrCapability);
6159
}
6260

6361
if (!$toolData->innerTool) {
64-
// @todo exception
65-
throw new \InvalidArgumentException('Capability must have an inner tool specified: ' . $toolData->toolOrCapability);
62+
throw InvalidToolTransferStringException::forCapabilityMissingInnerTool($toolData->toolOrCapability);
6663
}
6764

6865
$innerTool = $toolData->innerTool;

app/Services/Ai/Agents/Implementations/Chat/Values/ToolTransferData.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace App\Services\Ai\Agents\Implementations\Chat\Values;
66

7+
use App\Services\Ai\Agents\Exceptions\InvalidToolTransferStringException;
8+
79
readonly class ToolTransferData
810
{
911
private const string TYPE_CAPABILITY = 'capability';
@@ -42,13 +44,11 @@ public static function fromString(string $toolTransferString): self
4244
try {
4345
$settings = json_decode($settingsString, true, 512, JSON_THROW_ON_ERROR);
4446
if (!is_array($settings)) {
45-
// @todo exception
46-
throw new \InvalidArgumentException("Settings string is not a valid JSON object: {$settingsString}");
47+
throw InvalidToolTransferStringException::forSettingsNotJsonObject($settingsString);
4748
}
4849
return $settings;
4950
} catch (\JsonException $e) {
50-
// @todo exception
51-
throw new \InvalidArgumentException("Invalid JSON settings in tool transfer string: {$settingsString}", 0, $e);
51+
throw InvalidToolTransferStringException::forInvalidJsonSettings($settingsString, $e);
5252
}
5353
};
5454

@@ -58,8 +58,7 @@ public static function fromString(string $toolTransferString): self
5858
$settingsString = implode(':', array_slice($parts, 3));
5959

6060
if (empty($capabilityName) || empty($toolName)) {
61-
// @todo exception
62-
throw new \InvalidArgumentException("Invalid tool transfer string: {$toolTransferString}");
61+
throw InvalidToolTransferStringException::forMissingCapabilityOrToolName($toolTransferString);
6362
}
6463

6564
return new self(
@@ -75,8 +74,7 @@ public static function fromString(string $toolTransferString): self
7574
$settingsString = implode(':', array_slice($parts, 1));
7675

7776
if (empty($toolName)) {
78-
// @todo exception
79-
throw new \InvalidArgumentException("Invalid tool transfer string: {$toolTransferString}");
77+
throw InvalidToolTransferStringException::forEmptyToolName($toolTransferString);
8078
}
8179

8280
return new self(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace App\Services\Ai\Exceptions;
5+
6+
use App\Services\Ai\LaravelAi\ExtendedAiManager;
7+
8+
class InvalidAiManagerException extends \RuntimeException implements AiExceptionInterface
9+
{
10+
public static function forNotExtendedManager(): self
11+
{
12+
return new self(sprintf(
13+
'AiManager must be an instance of %s, but a different implementation is bound. '
14+
. 'Ensure the service provider registers %s correctly.',
15+
ExtendedAiManager::class,
16+
ExtendedAiManager::class
17+
));
18+
}
19+
}

0 commit comments

Comments
 (0)