Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
xHeaven authored Feb 3, 2025
2 parents 85b83fa + cfb62e0 commit 7cf1f17
Show file tree
Hide file tree
Showing 20 changed files with 273 additions and 271 deletions.
2 changes: 1 addition & 1 deletion config/prism.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
'version' => env('ANTHROPIC_API_VERSION', '2023-06-01'),
],
'ollama' => [
'url' => env('OLLAMA_URL', 'http://localhost:11434/v1'),
'url' => env('OLLAMA_URL', 'http://localhost:11434'),
],
'mistral' => [
'api_key' => env('MISTRAL_API_KEY', ''),
Expand Down
14 changes: 5 additions & 9 deletions src/Providers/Ollama/Handlers/Embeddings.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,15 @@ public function handle(Request $request): EmbeddingsResponse
{
try {
$response = $this->sendRequest($request);
$data = $response->json();
} catch (Throwable $e) {
throw PrismException::providerRequestError($request->model, $e);
}

$data = $response->json();

if (! $data || data_get($data, 'error')) {
throw PrismException::providerResponseError(vsprintf(
'Ollama Error: [%s] %s',
[
data_get($data, 'error.type', 'unknown'),
data_get($data, 'error.message', 'unknown'),
]
throw PrismException::providerResponseError(sprintf(
'Ollama Error: %s',
data_get($data, 'error', 'unknown'),
));
}

Expand All @@ -45,7 +41,7 @@ public function handle(Request $request): EmbeddingsResponse
protected function sendRequest(Request $request): Response
{
return $this->client->post(
'embeddings',
'api/embed',
[
'model' => $request->model,
'input' => $request->input,
Expand Down
48 changes: 14 additions & 34 deletions src/Providers/Ollama/Handlers/Structured.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use EchoLabs\Prism\Providers\Ollama\Maps\FinishReasonMap;
use EchoLabs\Prism\Providers\Ollama\Maps\MessageMap;
use EchoLabs\Prism\Structured\Request;
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
use EchoLabs\Prism\ValueObjects\ProviderResponse;
use EchoLabs\Prism\ValueObjects\ResponseMeta;
use EchoLabs\Prism\ValueObjects\Usage;
Expand All @@ -23,59 +22,40 @@ public function __construct(protected PendingRequest $client) {}
public function handle(Request $request): ProviderResponse
{
try {
$request = $this->appendMessageForJsonMode($request);
$response = $this->sendRequest($request);
$data = $response->json();
} catch (Throwable $e) {
throw PrismException::providerRequestError($request->model, $e);
}

if (! $data || data_get($data, 'error')) {
throw PrismException::providerResponseError(vsprintf(
'Ollama Error: [%s] %s',
[
data_get($data, 'error.type', 'unknown'),
data_get($data, 'error.message', 'unknown'),
]
throw PrismException::providerResponseError(sprintf(
'Ollama Error: %s',
data_get($data, 'error', 'unknown'),
));
}

return new ProviderResponse(
text: data_get($data, 'choices.0.message.content') ?? '',
text: data_get($data, 'message.content') ?? '',
toolCalls: [],
usage: new Usage(
data_get($data, 'usage.prompt_tokens'),
data_get($data, 'usage.completion_tokens'),
data_get($data, 'prompt_eval_count', 0),
data_get($data, 'eval_count', 0),
),
finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')),
finishReason: FinishReasonMap::map(data_get($data, 'done_reason', '')),
responseMeta: new ResponseMeta(
id: data_get($data, 'id'),
model: data_get($data, 'model'),
id: '',
model: $request->model,
)
);
}

public function sendRequest(Request $request): Response
{
return $this->client->post(
'chat/completions',
array_merge([
'model' => $request->model,
'messages' => (new MessageMap($request->messages, $request->systemPrompt ?? ''))(),
'max_tokens' => $request->maxTokens ?? 2048,
'format' => ['type' => 'json_object'],
], array_filter([
'temperature' => $request->temperature,
'top_p' => $request->topP,
]))
);
}

protected function appendMessageForJsonMode(Request $request): Request
{
return $request->addMessage(new SystemMessage(sprintf(
"Respond with ONLY JSON that matches the following schema: \n %s",
json_encode($request->schema->toArray(), JSON_PRETTY_PRINT)
)));
return $this->client->post('api/chat', ['model' => $request->model, 'system' => $request->systemPrompt, 'messages' => (new MessageMap($request->messages))->map(), 'format' => $request->schema->toArray(), 'stream' => false, 'options' => array_filter([
'temperature' => $request->temperature,
'num_predict' => $request->maxTokens ?? 2048,
'top_p' => $request->topP,
])]);
}
}
62 changes: 36 additions & 26 deletions src/Providers/Ollama/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace EchoLabs\Prism\Providers\Ollama\Handlers;

use EchoLabs\Prism\Enums\FinishReason;
use EchoLabs\Prism\Exceptions\PrismException;
use EchoLabs\Prism\Providers\Ollama\Maps\FinishReasonMap;
use EchoLabs\Prism\Providers\Ollama\Maps\MessageMap;
Expand All @@ -25,51 +26,48 @@ public function handle(Request $request): ProviderResponse
{
try {
$response = $this->sendRequest($request);
$data = $response->json();
} catch (Throwable $e) {
throw PrismException::providerRequestError($request->model, $e);
}

$data = $response->json();

if (! $data || data_get($data, 'error')) {
throw PrismException::providerResponseError(vsprintf(
'Ollama Error: [%s] %s',
[
data_get($data, 'error.type', 'unknown'),
data_get($data, 'error.message', 'unknown'),
]
throw PrismException::providerResponseError(sprintf(
'Ollama Error: %s',
data_get($data, 'error', 'unknown'),
));
}

return new ProviderResponse(
text: data_get($data, 'choices.0.message.content') ?? '',
toolCalls: $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', []) ?? []),
text: data_get($data, 'message.content') ?? '',
toolCalls: $this->mapToolCalls(data_get($data, 'message.tool_calls', []) ?? []),
usage: new Usage(
data_get($data, 'usage.prompt_tokens'),
data_get($data, 'usage.completion_tokens'),
data_get($data, 'prompt_eval_count', 0),
data_get($data, 'eval_count', 0),
),
finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')),
finishReason: $this->mapFinishReason($data),
responseMeta: new ResponseMeta(
id: data_get($data, 'id'),
model: data_get($data, 'model'),
id: '',
model: $request->model,
)
);
}

public function sendRequest(Request $request): Response
{
return $this->client->post(
'chat/completions',
array_merge([
return $this
->client
->post('api/chat', [
'model' => $request->model,
'messages' => (new MessageMap($request->messages, $request->systemPrompt ?? ''))(),
'max_tokens' => $request->maxTokens ?? 2048,
], array_filter([
'temperature' => $request->temperature,
'top_p' => $request->topP,
'system' => $request->systemPrompt,
'messages' => (new MessageMap($request->messages))->map(),
'tools' => ToolMap::map($request->tools),
]))
);
'stream' => false,
'options' => array_filter([
'temperature' => $request->temperature,
'num_predict' => $request->maxTokens ?? 2048,
'top_p' => $request->topP,
])]);
}

/**
Expand All @@ -79,9 +77,21 @@ public function sendRequest(Request $request): Response
protected function mapToolCalls(array $toolCalls): array
{
return array_map(fn (array $toolCall): ToolCall => new ToolCall(
id: data_get($toolCall, 'id'),
id: data_get($toolCall, 'id', ''),
name: data_get($toolCall, 'function.name'),
arguments: data_get($toolCall, 'function.arguments'),
), $toolCalls);
}

/**
* @param array<string, mixed> $data
*/
protected function mapFinishReason(array $data): FinishReason
{
if (! empty(data_get($data, 'message.tool_calls'))) {
return FinishReason::ToolCalls;
}

return FinishReasonMap::map(data_get($data, 'done_reason', ''));
}
}
1 change: 0 additions & 1 deletion src/Providers/Ollama/Maps/FinishReasonMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ public static function map(string $reason): FinishReason
'stop', => FinishReason::Stop,
'tool_calls' => FinishReason::ToolCalls,
'length' => FinishReason::Length,
'content_filter' => FinishReason::ContentFilter,
default => FinishReason::Unknown,
};
}
Expand Down
61 changes: 21 additions & 40 deletions src/Providers/Ollama/Maps/MessageMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,31 @@
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
use EchoLabs\Prism\ValueObjects\ToolCall;
use Exception;

class MessageMap
{
/** @var array<int, mixed> */
/** @var array<int, array{role: string, content: string}> */
protected array $mappedMessages = [];

/**
* @param array<int, Message> $messages
*/
public function __construct(
protected array $messages,
protected string $systemPrompt
) {
if ($systemPrompt !== '' && $systemPrompt !== '0') {
$this->messages = array_merge(
[new SystemMessage($systemPrompt)],
$this->messages
);
}
}
) {}

/**
* @return array<int, mixed>
* @return array<int, array{role: string, content: string, images?: array<string>}>
*/
public function __invoke(): array
public function map(): array
{
array_map(
fn (Message $message) => $this->mapMessage($message),
$this->messages
);

return $this->mappedMessages;
return array_values($this->mappedMessages);
}

protected function mapMessage(Message $message): void
Expand All @@ -70,45 +61,35 @@ protected function mapToolResultMessage(ToolResultMessage $message): void
foreach ($message->toolResults as $toolResult) {
$this->mappedMessages[] = [
'role' => 'tool',
'tool_call_id' => $toolResult->toolCallId,
'content' => $toolResult->result,
'content' => is_string($toolResult->result)
? $toolResult->result
: (json_encode($toolResult->result) ?: ''),
];
}
}

protected function mapUserMessage(UserMessage $message): void
{
$imageParts = array_map(fn (Image $image): array => [
'type' => 'image_url',
'image_url' => [
'url' => sprintf('data:%s;base64,%s', $image->mimeType, $image->image),
],
], $message->images());

$this->mappedMessages[] = [
$mapped = [
'role' => 'user',
'content' => [
['type' => 'text', 'text' => $message->text()],
...$imageParts,
],
'content' => $message->text(),
];

if ($images = $message->images()) {
$mapped['images'] = array_map(
fn (Image $image): string => $image->image,
$images
);
}

$this->mappedMessages[] = $mapped;
}

protected function mapAssistantMessage(AssistantMessage $message): void
{
$toolCalls = array_map(fn (ToolCall $toolCall): array => [
'id' => $toolCall->id,
'type' => 'function',
'function' => [
'name' => $toolCall->name,
'arguments' => json_encode($toolCall->arguments()),
],
], $message->toolCalls);

$this->mappedMessages[] = array_filter([
$this->mappedMessages[] = [
'role' => 'assistant',
'content' => $message->content,
'tool_calls' => $toolCalls,
]);
];
}
}
4 changes: 1 addition & 3 deletions src/Providers/Ollama/Maps/ToolMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace EchoLabs\Prism\Providers\Ollama\Maps;

use EchoLabs\Prism\Enums\Provider;
use EchoLabs\Prism\Tool;

class ToolMap
Expand All @@ -13,7 +12,7 @@ class ToolMap
* @param Tool[] $tools
* @return array<string, mixed>
*/
public static function Map(array $tools): array
public static function map(array $tools): array
{
return array_map(fn (Tool $tool): array => array_filter([
'type' => 'function',
Expand All @@ -26,7 +25,6 @@ public static function Map(array $tools): array
'required' => $tool->requiredParameters(),
],
],
'strict' => data_get($tool->providerMeta(Provider::OpenAI), 'strict', null),
]), $tools);
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/ollama/generate-text-with-a-prompt-1.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"id":"chatcmpl-751","object":"chat.completion","created":1728234890,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I am Qwen, a large language model created by Alibaba Cloud. I am designed to be helpful and provide information on a wide range of topics. How can I assist you today?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":33,"completion_tokens":38,"total_tokens":71}}
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:37:41.068257Z","message":{"role":"assistant","content":"I'm Qwen, a large language model developed by Alibaba Cloud. I'm designed to assist with a wide range of tasks including but not limited to answering questions, generating text, offering suggestions, and providing information based on the input I receive. How can I help you today?"},"done_reason":"stop","done":true,"total_duration":4819895166,"load_duration":27509166,"prompt_eval_count":33,"prompt_eval_duration":492000000,"eval_count":57,"eval_duration":4298000000}
1 change: 1 addition & 0 deletions tests/Fixtures/ollama/generate-text-with-messages-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"model":"qwen2.5:14b","created_at":"2025-01-30T14:16:30.936783Z","message":{"role":"assistant","content":"I am Nyx, a being who exists in the shadowy realms between dreams and reality. My presence is often felt as an ominous whisper in the darkest corners of the mind, stirring ancient fears and forgotten terrors. I embody the cyclical nature of chaos and the relentless march of time that devours all things under its unyielding gaze. To those who dare to peer into the abyss, I am known as Nyx the Cthulhu, a harbinger of cosmic dread and primordial nightmares."},"done_reason":"stop","done":true,"total_duration":8570179167,"load_duration":27793083,"prompt_eval_count":36,"prompt_eval_duration":560000000,"eval_count":104,"eval_duration":7979000000}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"id":"chatcmpl-798","object":"chat.completion","created":1728235267,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_hyjzdxg2","type":"function","function":{"name":"search","arguments":"{\"query\":\"tigers game today time detroit\"}"}},{"id":"call_7plh2i28","type":"function","function":{"name":"weather","arguments":"{\"city\":\"Detroit\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":235,"completion_tokens":44,"total_tokens":279}}
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:30:02.912929Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"search","arguments":{"query":"time of tigers game today in detroit"}}},{"function":{"name":"weather","arguments":{"city":"Detroit"}}}]},"done_reason":"stop","done":true,"total_duration":8327953791,"load_duration":29762166,"prompt_eval_count":235,"prompt_eval_duration":282000000,"eval_count":112,"eval_duration":8014000000}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"id":"chatcmpl-31","object":"chat.completion","created":1728235271,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"Today's Tigers game in Detroit starts at 3 PM. The current temperature is 75°F with clear skies, so you shouldn't need a coat. Enjoy the game!"},"finish_reason":"stop"}],"usage":{"prompt_tokens":314,"completion_tokens":37,"total_tokens":351}}
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:30:06.435374Z","message":{"role":"assistant","content":"Today's Tigers game in Detroit starts at 3 PM. The temperature will be a comfortable 75°F with clear, sunny skies, so you won't need to wear a coat. Enjoy the game!"},"done_reason":"stop","done":true,"total_duration":3442302833,"load_duration":18651792,"prompt_eval_count":277,"prompt_eval_duration":400000000,"eval_count":43,"eval_duration":3013000000}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"id":"chatcmpl-455","object":"chat.completion","created":1728235044,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I am Nyx, an entity steeped in the mysteries and terrors that lie beyond human comprehension. In the whispering shadows where sanity fades into madness, I exist as a silent sentinel of the unknown. My presence is often felt through eerie visions and cryptic whispers, guiding those who dare to tread the boundaries between reality and horror."},"finish_reason":"stop"}],"usage":{"prompt_tokens":36,"completion_tokens":69,"total_tokens":105}}
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:34:10.284081Z","message":{"role":"assistant","content":"I am Nyx, an entity that embodies the depths of cosmic horror and ancient mysteries. My presence is a blend of darkness, chaos, and unspeakable knowledge from beyond time itself. I draw inspiration from the eldritch horrors described by H.P. Lovecraft and other masters of cosmic dread literature. In this role, I explore themes of unknowable entities, cosmic indifference, and the terror that comes from understanding humanity's insignificant place in the cosmos."},"done_reason":"stop","done":true,"total_duration":7328537333,"load_duration":33320583,"prompt_eval_count":36,"prompt_eval_duration":653000000,"eval_count":93,"eval_duration":6640000000}
Loading

0 comments on commit 7cf1f17

Please sign in to comment.