Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit 71e7916

Browse files
feat: adding llama support via replicate and ollama
feat: LLama prompt (#135) * Llama prompt * - * - * - * - feat: wrap up ollama example Update src/Platform/Replicate.php Co-authored-by: Oskar Stark <oskarstark@googlemail.com>
1 parent d706360 commit 71e7916

File tree

8 files changed

+439
-0
lines changed

8 files changed

+439
-0
lines changed

.env

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ ANTHROPIC_API_KEY=
99
# For using Voyage
1010
VOYAGE_API_KEY=
1111

12+
# For using Replicate
13+
REPLICATE_API_KEY=
14+
15+
# For using Ollama
16+
OLLAMA_HOST_URL=
17+
1218
# For using GPT on Azure
1319
AZURE_OPENAI_BASEURL=
1420
AZURE_OPENAI_DEPLOYMENT=

examples/chat-llama-ollama.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain;
4+
use PhpLlm\LlmChain\Message\Message;
5+
use PhpLlm\LlmChain\Message\MessageBag;
6+
use PhpLlm\LlmChain\Model\Language\Llama;
7+
use PhpLlm\LlmChain\Platform\Ollama;
8+
use Symfony\Component\Dotenv\Dotenv;
9+
use Symfony\Component\HttpClient\HttpClient;
10+
11+
require_once dirname(__DIR__).'/vendor/autoload.php';
12+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
13+
14+
if (empty($_ENV['OLLAMA_HOST_URL'])) {
15+
echo 'Please set the OLLAMA_HOST_URL environment variable.'.PHP_EOL;
16+
exit(1);
17+
}
18+
19+
$platform = new Ollama(HttpClient::create(), $_ENV['OLLAMA_HOST_URL']);
20+
$llm = new Llama($platform);
21+
22+
$chain = new Chain($llm);
23+
$messages = new MessageBag(
24+
Message::forSystem('You are a helpful assistant.'),
25+
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
26+
);
27+
$response = $chain->call($messages);
28+
29+
echo $response->getContent().PHP_EOL;

examples/chat-llama-replicate.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain;
4+
use PhpLlm\LlmChain\Message\Message;
5+
use PhpLlm\LlmChain\Message\MessageBag;
6+
use PhpLlm\LlmChain\Model\Language\Llama;
7+
use PhpLlm\LlmChain\Platform\Replicate;
8+
use Symfony\Component\Dotenv\Dotenv;
9+
use Symfony\Component\HttpClient\HttpClient;
10+
11+
require_once dirname(__DIR__).'/vendor/autoload.php';
12+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
13+
14+
if (empty($_ENV['REPLICATE_API_KEY'])) {
15+
echo 'Please set the REPLICATE_API_KEY environment variable.'.PHP_EOL;
16+
exit(1);
17+
}
18+
19+
$platform = new Replicate(HttpClient::create(), $_ENV['REPLICATE_API_KEY']);
20+
$llm = new Llama($platform);
21+
22+
$chain = new Chain($llm);
23+
$messages = new MessageBag(
24+
Message::forSystem('You are a helpful assistant.'),
25+
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
26+
);
27+
$response = $chain->call($messages);
28+
29+
echo $response->getContent().PHP_EOL;

ollama.http

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### Llama 3.2 on Ollama
2+
POST http://localhost:11434/api/chat
3+
4+
{
5+
"model": "llama3.2",
6+
"messages": [
7+
{
8+
"role": "user",
9+
"content": "why is the sky blue?"
10+
}
11+
],
12+
"stream": false
13+
}

src/Model/Language/Llama.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Model\Language;
6+
7+
use PhpLlm\LlmChain\Exception\RuntimeException;
8+
use PhpLlm\LlmChain\LanguageModel;
9+
use PhpLlm\LlmChain\Message\AssistantMessage;
10+
use PhpLlm\LlmChain\Message\Content\Image;
11+
use PhpLlm\LlmChain\Message\Content\Text;
12+
use PhpLlm\LlmChain\Message\MessageBag;
13+
use PhpLlm\LlmChain\Message\SystemMessage;
14+
use PhpLlm\LlmChain\Message\UserMessage;
15+
use PhpLlm\LlmChain\Platform\Ollama;
16+
use PhpLlm\LlmChain\Platform\Replicate;
17+
use PhpLlm\LlmChain\Response\TextResponse;
18+
19+
final readonly class Llama implements LanguageModel
20+
{
21+
public function __construct(
22+
private Replicate|Ollama $platform,
23+
) {
24+
}
25+
26+
public function call(MessageBag $messages, array $options = []): TextResponse
27+
{
28+
if ($this->platform instanceof Replicate) {
29+
$response = $this->platform->request('meta/meta-llama-3.1-405b-instruct', 'predictions', [
30+
'system' => self::convertMessage($messages->getSystemMessage() ?? new SystemMessage('')),
31+
'prompt' => self::convertToPrompt($messages->withoutSystemMessage()),
32+
]);
33+
34+
return new TextResponse(implode('', $response['output']));
35+
}
36+
37+
$response = $this->platform->request('llama3.2', 'chat', ['messages' => $messages, 'stream' => false]);
38+
39+
return new TextResponse($response['message']['content']);
40+
}
41+
42+
/**
43+
* @todo make method private, just for testing, or create a MessageBag to LLama convert class :thinking:
44+
*/
45+
public static function convertToPrompt(MessageBag $messageBag): string
46+
{
47+
$messages = [];
48+
49+
/** @var UserMessage|SystemMessage|AssistantMessage $message */
50+
foreach ($messageBag->getIterator() as $message) {
51+
$messages[] = self::convertMessage($message);
52+
}
53+
54+
$messages = array_filter($messages, fn ($message) => '' !== $message);
55+
56+
return trim(implode(PHP_EOL.PHP_EOL, $messages)).PHP_EOL.PHP_EOL.'<|start_header_id|>assistant<|end_header_id|>';
57+
}
58+
59+
/**
60+
* @todo make method private, just for testing
61+
*/
62+
public static function convertMessage(UserMessage|SystemMessage|AssistantMessage $message): string
63+
{
64+
if ($message instanceof SystemMessage) {
65+
return trim(<<<SYSTEM
66+
<|begin_of_text|><|start_header_id|>system<|end_header_id|>
67+
68+
{$message->content}<|eot_id|>
69+
SYSTEM);
70+
}
71+
72+
if ($message instanceof AssistantMessage) {
73+
if ('' === $message->content || null === $message->content) {
74+
return '';
75+
}
76+
77+
return trim(<<<ASSISTANT
78+
<|start_header_id|>{$message->getRole()->value}<|end_header_id|>
79+
80+
{$message->content}<|eot_id|>
81+
ASSISTANT);
82+
}
83+
84+
if ($message instanceof UserMessage) {
85+
$count = count($message->content);
86+
87+
$contentParts = [];
88+
if ($count > 1) {
89+
foreach ($message->content as $value) {
90+
if ($value instanceof Text) {
91+
$contentParts[] = $value->text;
92+
}
93+
94+
if ($value instanceof Image) {
95+
$contentParts[] = $value->url;
96+
}
97+
}
98+
} elseif (1 === $count) {
99+
$value = $message->content[0];
100+
if ($value instanceof Text) {
101+
$contentParts[] = $value->text;
102+
}
103+
104+
if ($value instanceof Image) {
105+
$contentParts[] = $value->url;
106+
}
107+
} else {
108+
throw new RuntimeException('Unsupported message type.');
109+
}
110+
111+
$content = implode(PHP_EOL, $contentParts);
112+
113+
return trim(<<<USER
114+
<|start_header_id|>{$message->getRole()->value}<|end_header_id|>
115+
116+
{$content}<|eot_id|>
117+
USER);
118+
}
119+
120+
throw new RuntimeException('Unsupported message type.'); // @phpstan-ignore-line
121+
}
122+
123+
public function supportsToolCalling(): bool
124+
{
125+
return false; // it does, but implementation here is still open.
126+
}
127+
128+
public function supportsImageInput(): bool
129+
{
130+
return false; // it does, but implementation here is still open.
131+
}
132+
133+
public function supportsStructuredOutput(): bool
134+
{
135+
return false;
136+
}
137+
}

src/Platform/Ollama.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform;
6+
7+
use Symfony\Contracts\HttpClient\HttpClientInterface;
8+
9+
final readonly class Ollama
10+
{
11+
public function __construct(
12+
private HttpClientInterface $httpClient,
13+
private string $hostUrl,
14+
) {
15+
}
16+
17+
/**
18+
* @param string $model The model name on Replicate, e.g. "meta/meta-llama-3.1-405b-instruct"
19+
* @param array<string, mixed> $body
20+
*
21+
* @return array<string, mixed>
22+
*/
23+
public function request(string $model, string $endpoint, array $body): array
24+
{
25+
$url = sprintf('%s/api/%s', $this->hostUrl, $endpoint);
26+
27+
$response = $this->httpClient->request('POST', $url, [
28+
'headers' => ['Content-Type' => 'application/json'],
29+
'json' => array_merge($body, [
30+
'model' => $model,
31+
]),
32+
]);
33+
34+
return $response->toArray();
35+
}
36+
}

src/Platform/Replicate.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform;
6+
7+
use Symfony\Contracts\HttpClient\HttpClientInterface;
8+
9+
final readonly class Replicate
10+
{
11+
public function __construct(
12+
private HttpClientInterface $httpClient,
13+
#[\SensitiveParameter] private string $apiKey,
14+
) {
15+
}
16+
17+
/**
18+
* @param string $model The model name on Replicate, e.g. "meta/meta-llama-3.1-405b-instruct"
19+
* @param array<string, mixed> $body
20+
*
21+
* @return array<string, mixed>
22+
*/
23+
public function request(string $model, string $endpoint, array $body): array
24+
{
25+
$url = sprintf('https://api.replicate.com/v1/models/%s/%s', $model, $endpoint);
26+
27+
$response = $this->httpClient->request('POST', $url, [
28+
'headers' => ['Content-Type' => 'application/json'],
29+
'auth_bearer' => $this->apiKey,
30+
'json' => ['input' => $body],
31+
])->toArray();
32+
33+
while (!in_array($response['status'], ['succeeded', 'failed', 'canceled'], true)) {
34+
sleep(1);
35+
36+
$response = $this->getResponse($response['id']);
37+
}
38+
39+
return $response;
40+
}
41+
42+
/**
43+
* @return array<string, mixed>
44+
*/
45+
private function getResponse(string $id): array
46+
{
47+
$url = sprintf('https://api.replicate.com/v1/predictions/%s', $id);
48+
49+
$response = $this->httpClient->request('GET', $url, [
50+
'headers' => ['Content-Type' => 'application/json'],
51+
'auth_bearer' => $this->apiKey,
52+
]);
53+
54+
return $response->toArray();
55+
}
56+
}

0 commit comments

Comments
 (0)