diff --git a/app/Integrations/Ollama/Requests/ChatRequest.php b/app/Integrations/Ollama/Requests/ChatRequest.php index 8741a8e..bae8f30 100644 --- a/app/Integrations/Ollama/Requests/ChatRequest.php +++ b/app/Integrations/Ollama/Requests/ChatRequest.php @@ -3,7 +3,10 @@ namespace App\Integrations\Ollama\Requests; use App\Data\MessageData; +use App\Data\ToolCallData; +use App\Data\ToolFunctionData; use App\Models\Thread; +use Illuminate\Support\Str; use JsonException; use Saloon\Contracts\Body\HasBody; use Saloon\Enums\Method; @@ -31,27 +34,84 @@ public function defaultBody(): array { $assistant = $this->thread->project->assistant; - return [ + $body = [ 'model' => $assistant->model, 'messages' => $this->formatMessages($assistant), 'stream' => false, 'raw' => true, - 'tools' => array_values($this->tools), + 'tools' => $this->formatTools(), ]; + + + return $body; } private function formatMessages($assistant): array { $systemMessage = [ 'role' => 'system', - 'content' => sprintf( - '[INST]%s[/INST] [AVAILABLE_TOOLS]%s[/AVAILABLE_TOOLS]', - $assistant->prompt, - json_encode(array_values($this->tools)) - ), + 'content' => $assistant->prompt, ]; - return [$systemMessage, ...$this->thread->messages->toArray()]; + $formattedMessages = [$systemMessage]; + + foreach ($this->thread->messages as $message) { + $formattedMessage = [ + 'role' => $message['role'], + 'content' => $message['content'], + ]; + + if (!empty($message['tool_calls'])) { + $formattedMessage['tool_calls'] = $this->formatToolCalls($message['tool_calls']); + } + + $formattedMessages[] = $formattedMessage; + } + + return $formattedMessages; + } + + private function formatToolCalls($toolCalls): array + { + return array_map(function ($toolCall) { + $function = $toolCall['function']; + + // Ensure arguments is a JSON object string + $arguments = is_string($function['arguments']) + ? json_decode($function['arguments'], true) // Decode string to ensure it's valid JSON + : json_encode($function['arguments'], JSON_FORCE_OBJECT); // Encode array/object to JSON object + + return [ + 'id' => $toolCall['id'], + 'type' => 'function', + 'function' => [ + 'name' => $function['name'], + 'arguments' => $arguments, // Always a JSON object + ], + ]; + }, $toolCalls); + } + + private function formatTools(): array + { + + $formattedTools = []; + foreach ($this->tools as $key => $tool) { + if (is_array($tool) && isset($tool['function'])) { + $formattedTools[] = [ + 'type' => 'function', + 'function' => [ + 'name' => $tool['function']['name'] ?? $key, + 'description' => $tool['function']['description'] ?? '', + 'parameters' => $tool['function']['parameters'] ?? [], + ], + ]; + } else { + } + } + + + return $formattedTools; } /** @@ -60,6 +120,34 @@ private function formatMessages($assistant): array public function createDtoFromResponse(Response $response): MessageData { $data = $response->json(); - return MessageData::from($data['message'] ?? []); + $message = $data['message'] ?? []; + $tools = collect(); + + if (isset($message['tool_calls'])) { + foreach ($message['tool_calls'] as $toolCall) { + $arguments = $toolCall['function']['arguments']; + // Ensure arguments is a JSON string + if (is_array($arguments)) { + $arguments = json_encode($arguments, JSON_FORCE_OBJECT); + } elseif (!is_string($arguments)) { + $arguments = json_encode([$arguments], JSON_FORCE_OBJECT); + } + + $fn = ToolFunctionData::from([ + 'name' => $toolCall['function']['name'], + 'arguments' => $arguments + ]); + + $tools->push(ToolCallData::from([ + 'id' => $toolCall['id'] ?? 'ollama-'.Str::random(10), + 'type' => 'function', + 'function' => $fn + ])); + } + + $message['tool_calls'] = $tools; + } + + return MessageData::from($message); } } diff --git a/app/Services/ChatAssistant.php b/app/Services/ChatAssistant.php index debccc1..49313d6 100644 --- a/app/Services/ChatAssistant.php +++ b/app/Services/ChatAssistant.php @@ -179,8 +179,18 @@ private function handleTools($thread, $message): string { $answer = $message->content; - $thread->messages()->create($message->toArray()); - if ($message->tool_calls !== null && $message->tool_calls->isNotEmpty()) { + $messageData = [ + 'role' => $message->role, + 'content' => $message->content, + ]; + + if (!empty($message->tool_calls)) { + $messageData['tool_calls'] = $message->tool_calls; + } + + $thread->messages()->create($messageData); + + if (!empty($message->tool_calls)) { $this->renderAnswer($answer); foreach ($message->tool_calls as $toolCall) { @@ -284,9 +294,16 @@ private function executeToolCall($thread, $toolCall): void $thread->messages()->create([ 'role' => 'tool', - 'tool_call_id' => $toolCall->id, - 'name' => $toolCall->function->name, 'content' => $toolResponse, + 'tool_calls' => [ + [ + 'id' => $toolCall->id, + 'function' => [ + 'name' => $toolCall->function->name, + 'arguments' => $toolCall->function->arguments, + ], + ], + ], ]); } catch (Exception $e) { throw new Exception("Error calling tool: {$e->getMessage()}"); @@ -300,4 +317,4 @@ private function ensureAPIKey(string $service): void $this->onBoardingSteps->requestAPIKey($service); } } -} +} \ No newline at end of file diff --git a/app/Services/Request/ChatRequest.php b/app/Services/Request/ChatRequest.php index ed38434..5a0c569 100644 --- a/app/Services/Request/ChatRequest.php +++ b/app/Services/Request/ChatRequest.php @@ -40,9 +40,18 @@ public function defaultBody(): array $messages = [[ 'role' => 'system', 'content' => $assistant->prompt, - ], - ...$this->thread->messages, - ]; + ]]; + + foreach ($this->thread->messages as $message) { + $messages[] = [ + 'role' => $message->role, + 'content' => $message->content, + ]; + + if ($message->tool_calls) { + $messages[count($messages) - 1]['tool_calls'] = $message->tool_calls; + } + } return [ 'model' => $assistant->model, @@ -50,4 +59,4 @@ public function defaultBody(): array 'tools' => array_values($this->tools), ]; } -} +} \ No newline at end of file diff --git a/app/Utils/OnBoardingSteps.php b/app/Utils/OnBoardingSteps.php index 4ebb0fa..9bff810 100644 --- a/app/Utils/OnBoardingSteps.php +++ b/app/Utils/OnBoardingSteps.php @@ -55,6 +55,9 @@ private function configurationFileExists(): bool return true; } + /** + * @throws Exception + */ public function requestAPIKey(string $service): string { $apiKey = password( diff --git a/config/database.php b/config/database.php index 8a5ea57..a8b0fce 100644 --- a/config/database.php +++ b/config/database.php @@ -39,6 +39,14 @@ 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], + 'sqlite-dev' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + 'mysql' => [ 'driver' => 'mysql', 'url' => env('DB_URL'),