Skip to content

Commit 12dcbd1

Browse files
authored
Request image support (#42)
1 parent 2bcc28e commit 12dcbd1

31 files changed

+1086
-437
lines changed

Diff for: src/Contracts/Message.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,4 @@
44

55
namespace EchoLabs\Prism\Contracts;
66

7-
interface Message
8-
{
9-
public function content(): string;
10-
}
7+
interface Message {}

Diff for: src/Http/Controllers/PrismChatController.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace EchoLabs\Prism\Http\Controllers;
44

5-
use EchoLabs\Prism\Contracts\Message;
65
use EchoLabs\Prism\Exceptions\PrismServerException;
76
use EchoLabs\Prism\Facades\PrismServer;
87
use EchoLabs\Prism\Generators\TextGenerator;
@@ -113,8 +112,8 @@ protected function chat(TextGenerator $generator): Response
113112
protected function textFromResponse(TextResponse $response): string
114113
{
115114
return $response->responseMessages
116-
->filter(fn (Message $message): bool => $message instanceof AssistantMessage)
117-
->implode(fn (Message $message): string => $message->content(), "\n");
115+
->whereInstanceOf(AssistantMessage::class)
116+
->implode(fn (AssistantMessage $message): string => $message->content);
118117
}
119118

120119
/**

Diff for: src/Providers/Anthropic/MessageMap.php

+28-7
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
use EchoLabs\Prism\Contracts\Message;
88
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
9+
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
910
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1011
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
1112
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
1213
use EchoLabs\Prism\ValueObjects\ToolCall;
1314
use EchoLabs\Prism\ValueObjects\ToolResult;
1415
use Exception;
16+
use InvalidArgumentException;
1517

1618
class MessageMap
1719
{
@@ -58,13 +60,32 @@ protected function mapToolResultMessage(ToolResultMessage $message): array
5860
}
5961

6062
/**
61-
* @return array<string, string>
63+
* @return array<string, mixed>
6264
*/
6365
protected function mapUserMessage(UserMessage $message): array
6466
{
67+
$imageParts = array_map(function (Image $image): array {
68+
if ($image->isUrl()) {
69+
throw new InvalidArgumentException('URL image type is not supported by Anthropic');
70+
}
71+
72+
return [
73+
'type' => 'image',
74+
'source' => [
75+
'type' => 'base64',
76+
'media_type' => $image->mimeType,
77+
'data' => $image->image,
78+
],
79+
];
80+
}, $message->images());
81+
6582
return [
6683
'role' => 'user',
67-
'content' => $message->content(),
84+
'content' => [
85+
['type' => 'text', 'text' => $message->text()],
86+
...$imageParts,
87+
],
88+
6889
];
6990
}
7091

@@ -73,13 +94,13 @@ protected function mapUserMessage(UserMessage $message): array
7394
*/
7495
protected function mapAssistantMessage(AssistantMessage $message): array
7596
{
76-
if ($message->hasToolCall()) {
97+
if ($message->toolCalls) {
7798
$content = [];
7899

79-
if ($message->content() !== '' && $message->content() !== '0') {
100+
if ($message->content !== '' && $message->content !== '0') {
80101
$content[] = [
81102
'type' => 'text',
82-
'text' => $message->content(),
103+
'text' => $message->content,
83104
];
84105
}
85106

@@ -88,7 +109,7 @@ protected function mapAssistantMessage(AssistantMessage $message): array
88109
'id' => $toolCall->id,
89110
'name' => $toolCall->name,
90111
'input' => $toolCall->arguments(),
91-
], $message->toolCalls());
112+
], $message->toolCalls);
92113

93114
return [
94115
'role' => 'assistant',
@@ -98,7 +119,7 @@ protected function mapAssistantMessage(AssistantMessage $message): array
98119

99120
return [
100121
'role' => 'assistant',
101-
'content' => $message->content(),
122+
'content' => $message->content,
102123
];
103124
}
104125
}

Diff for: src/Providers/Mistral/MessageMap.php

+18-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
use EchoLabs\Prism\Contracts\Message;
88
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
9+
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
910
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1011
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
1112
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
1213
use EchoLabs\Prism\ValueObjects\ToolCall;
1314
use Exception;
15+
use Illuminate\Support\Str;
1416

1517
class MessageMap
1618
{
@@ -60,7 +62,7 @@ protected function mapSystemMessage(SystemMessage $message): void
6062
{
6163
$this->mappedMessages[] = [
6264
'role' => 'system',
63-
'content' => $message->content(),
65+
'content' => $message->content,
6466
];
6567
}
6668

@@ -77,9 +79,21 @@ protected function mapToolResultMessage(ToolResultMessage $message): void
7779

7880
protected function mapUserMessage(UserMessage $message): void
7981
{
82+
$imageParts = array_map(fn (Image $part): array => [
83+
'type' => 'image_url',
84+
'image_url' => [
85+
'url' => Str::isUrl($part->image)
86+
? $part->image
87+
: sprintf('data:%s;base64,%s', $part->mimeType ?? 'image/jpeg', $part->image),
88+
],
89+
], $message->images());
90+
8091
$this->mappedMessages[] = [
8192
'role' => 'user',
82-
'content' => $message->content(),
93+
'content' => [
94+
['type' => 'text', 'text' => $message->text()],
95+
...$imageParts,
96+
],
8397
];
8498
}
8599

@@ -92,11 +106,11 @@ protected function mapAssistantMessage(AssistantMessage $message): void
92106
'name' => $toolCall->name,
93107
'arguments' => json_encode($toolCall->arguments()),
94108
],
95-
], $message->toolCalls());
109+
], $message->toolCalls);
96110

97111
$this->mappedMessages[] = array_filter([
98112
'role' => 'assistant',
99-
'content' => $message->content(),
113+
'content' => $message->content,
100114
'tool_calls' => $toolCalls,
101115
]);
102116
}

Diff for: src/Providers/Mistral/Mistral.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ public function text(TextRequest $request): ProviderResponse
4343

4444
$data = $response->json();
4545

46-
if (data_get($data, 'error') || ! $data) {
46+
if (data_get($data, 'message') || ! $data) {
4747
throw PrismException::providerResponseError(vsprintf(
48-
'Mistral Error: [%s] %s',
48+
'Mistral Error: %s',
4949
[
50-
data_get($data, 'error.type', 'unknown'),
51-
data_get($data, 'error.message', 'unknown'),
50+
data_get($data, 'message', 'unknown'),
5251
]
5352
));
5453
}

Diff for: src/Providers/Ollama/MessageMap.php

+22-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
use EchoLabs\Prism\Contracts\Message;
88
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
9+
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
910
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1011
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
1112
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
1213
use EchoLabs\Prism\ValueObjects\ToolCall;
1314
use Exception;
15+
use InvalidArgumentException;
1416

1517
class MessageMap
1618
{
@@ -60,7 +62,7 @@ protected function mapSystemMessage(SystemMessage $message): void
6062
{
6163
$this->mappedMessages[] = [
6264
'role' => 'system',
63-
'content' => $message->content(),
65+
'content' => $message->content,
6466
];
6567
}
6668

@@ -77,9 +79,25 @@ protected function mapToolResultMessage(ToolResultMessage $message): void
7779

7880
protected function mapUserMessage(UserMessage $message): void
7981
{
82+
$imageParts = array_map(function (Image $image): array {
83+
if ($image->isUrl()) {
84+
throw new InvalidArgumentException('URL image type is not supported by Ollama');
85+
}
86+
87+
return [
88+
'type' => 'image_url',
89+
'image_url' => [
90+
'url' => sprintf('data:%s;base64,%s', $image->mimeType, $image->image),
91+
],
92+
];
93+
}, $message->images());
94+
8095
$this->mappedMessages[] = [
8196
'role' => 'user',
82-
'content' => $message->content(),
97+
'content' => [
98+
['type' => 'text', 'text' => $message->text()],
99+
...$imageParts,
100+
],
83101
];
84102
}
85103

@@ -92,11 +110,11 @@ protected function mapAssistantMessage(AssistantMessage $message): void
92110
'name' => $toolCall->name,
93111
'arguments' => json_encode($toolCall->arguments()),
94112
],
95-
], $message->toolCalls());
113+
], $message->toolCalls);
96114

97115
$this->mappedMessages[] = array_filter([
98116
'role' => 'assistant',
99-
'content' => $message->content(),
117+
'content' => $message->content,
100118
'tool_calls' => $toolCalls,
101119
]);
102120
}

Diff for: src/Providers/OpenAI/MessageMap.php

+17-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use EchoLabs\Prism\Contracts\Message;
88
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
9+
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
910
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1011
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
1112
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
@@ -60,7 +61,7 @@ protected function mapSystemMessage(SystemMessage $message): void
6061
{
6162
$this->mappedMessages[] = [
6263
'role' => 'system',
63-
'content' => $message->content(),
64+
'content' => $message->content,
6465
];
6566
}
6667

@@ -77,9 +78,21 @@ protected function mapToolResultMessage(ToolResultMessage $message): void
7778

7879
protected function mapUserMessage(UserMessage $message): void
7980
{
81+
$imageParts = array_map(fn (Image $image): array => [
82+
'type' => 'image_url',
83+
'image_url' => [
84+
'url' => $image->isUrl()
85+
? $image->image
86+
: sprintf('data:%s;base64,%s', $image->mimeType, $image->image),
87+
],
88+
], $message->images());
89+
8090
$this->mappedMessages[] = [
8191
'role' => 'user',
82-
'content' => $message->content(),
92+
'content' => [
93+
['type' => 'text', 'text' => $message->text()],
94+
...$imageParts,
95+
],
8396
];
8497
}
8598

@@ -92,11 +105,11 @@ protected function mapAssistantMessage(AssistantMessage $message): void
92105
'name' => $toolCall->name,
93106
'arguments' => json_encode($toolCall->arguments()),
94107
],
95-
], $message->toolCalls());
108+
], $message->toolCalls);
96109

97110
$this->mappedMessages[] = array_filter([
98111
'role' => 'assistant',
99-
'content' => $message->content(),
112+
'content' => $message->content,
100113
'tool_calls' => $toolCalls,
101114
]);
102115
}

Diff for: src/ValueObjects/Messages/AssistantMessage.php

+3-22
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,10 @@
1010
class AssistantMessage implements Message
1111
{
1212
/**
13-
* @param array<int, ToolCall> $toolCalls
13+
* @param ToolCall[] $toolCalls
1414
*/
1515
public function __construct(
16-
protected readonly string $content = '',
17-
protected array $toolCalls = []
16+
public readonly string $content,
17+
public readonly array $toolCalls = []
1818
) {}
19-
20-
#[\Override]
21-
public function content(): string
22-
{
23-
return $this->content;
24-
}
25-
26-
public function hasToolCall(): bool
27-
{
28-
return $this->toolCalls !== [];
29-
}
30-
31-
/**
32-
* @return array<int, ToolCall> $toolCalls
33-
*/
34-
public function toolCalls(): array
35-
{
36-
return $this->toolCalls;
37-
}
3819
}

Diff for: src/ValueObjects/Messages/Support/Image.php

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace EchoLabs\Prism\ValueObjects\Messages\Support;
6+
7+
use Illuminate\Support\Facades\File;
8+
use Illuminate\Support\Str;
9+
use InvalidArgumentException;
10+
11+
class Image
12+
{
13+
public function __construct(
14+
public readonly string $image,
15+
public readonly ?string $mimeType = null,
16+
) {}
17+
18+
public static function fromPath(string $path): self
19+
{
20+
if (! is_file($path)) {
21+
throw new InvalidArgumentException("{$path} is not a file");
22+
}
23+
24+
$content = file_get_contents($path);
25+
26+
if ($content === '' || $content === '0' || $content === false) {
27+
throw new InvalidArgumentException("{$path} is empty");
28+
}
29+
30+
return new self(
31+
base64_encode($content),
32+
File::mimeType($path) ?: null,
33+
);
34+
}
35+
36+
public static function fromUrl(string $url): self
37+
{
38+
return new self($url);
39+
}
40+
41+
public static function fromBase64(string $image, string $mimeType): self
42+
{
43+
return new self(
44+
$image,
45+
$mimeType
46+
);
47+
}
48+
49+
public function isUrl(): bool
50+
{
51+
return Str::isUrl($this->image);
52+
}
53+
}

0 commit comments

Comments
 (0)