diff --git a/README.md b/README.md index ccf51c1a..e1548e67 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,12 @@ And just thinking how to make some of those flows work in a Laravel environment Per the Laravel docs https://laravel.com/docs/11.x/reverb + + +## Local Dev + +```bash +php artisan horizon:watch +php artisan reverb:start --debug +npm run dev +``` \ No newline at end of file diff --git a/app/Domains/Messages/SearchOrSummarizeChatRepo.php b/app/Domains/Messages/SearchOrSummarizeChatRepo.php index d91a6f97..cbca222e 100644 --- a/app/Domains/Messages/SearchOrSummarizeChatRepo.php +++ b/app/Domains/Messages/SearchOrSummarizeChatRepo.php @@ -23,7 +23,7 @@ public function search(Chat $chat, string $input): string /** @var EmbeddingsResponseDto $embedding */ $embedding = LlmDriverFacade::driver( - $chat->chatable->getDriver() + $chat->chatable->getEmbeddingDriver() )->embedData($input); $results = DocumentChunk::query() @@ -40,12 +40,12 @@ public function search(Chat $chat, string $input): string $content = []; foreach ($results as $result) { - $content[] = reduce_text_size($result->content); + $content[] = remove_ascii(reduce_text_size($result->content)); //reduce_text_size seem to mess up Claude? } $content = implode(' ', $content); - $content = 'This is data from the search results when entering the users prompt please use this for context and only this: '.$content; + $content = 'This is data from the search results when entering the users prompt please use this for context and only this and return as markdown so I can render it: '.$content; $chat->addInput( message: $content, diff --git a/app/Http/Controllers/CollectionController.php b/app/Http/Controllers/CollectionController.php index d79565d4..e92c7bb4 100644 --- a/app/Http/Controllers/CollectionController.php +++ b/app/Http/Controllers/CollectionController.php @@ -31,6 +31,7 @@ public function store() 'name' => 'required', 'description' => 'required', 'driver' => 'required', + 'embedding_driver' => 'required', ]); $validated['team_id'] = auth()->user()->current_team_id; diff --git a/app/Jobs/VectorlizeDataJob.php b/app/Jobs/VectorlizeDataJob.php index 868762a5..97811ab6 100644 --- a/app/Jobs/VectorlizeDataJob.php +++ b/app/Jobs/VectorlizeDataJob.php @@ -43,7 +43,7 @@ public function handle(): void $content = $this->documentChunk->content; /** @var EmbeddingsResponseDto $results */ - $results = LlmDriverFacade::driver($this->documentChunk->getDriver()) + $results = LlmDriverFacade::driver($this->documentChunk->getEmbeddingDriver()) ->embedData($content); $this->documentChunk->update([ diff --git a/app/LlmDriver/ClaudeClient.php b/app/LlmDriver/ClaudeClient.php index 6bf9fe98..042208dc 100644 --- a/app/LlmDriver/ClaudeClient.php +++ b/app/LlmDriver/ClaudeClient.php @@ -5,25 +5,21 @@ use App\LlmDriver\Requests\MessageInDto; use App\LlmDriver\Responses\CompletionResponse; use App\LlmDriver\Responses\EmbeddingsResponseDto; +use Illuminate\Http\Client\Response; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class ClaudeClient { + protected string $baseUrl = 'https://api.anthropic.com/v1'; + + protected string $version = '2023-06-01'; + protected string $driver = 'claude'; public function embedData(string $data): EmbeddingsResponseDto { - if (! app()->environment('testing')) { - sleep(2); - } - Log::info('LlmDriver::MockClient::embedData'); - - $data = get_fixture('embedding_response.json'); - - return EmbeddingsResponseDto::from([ - 'embedding' => data_get($data, 'data.0.embedding'), - 'token_count' => 1000, - ]); + throw new \Exception('Not implemented'); } /** @@ -31,32 +27,118 @@ public function embedData(string $data): EmbeddingsResponseDto */ public function chat(array $messages): CompletionResponse { - if (! app()->environment('testing')) { - sleep(2); + $model = $this->getConfig('claude')['models']['completion_model']; + $maxTokens = $this->getConfig('claude')['max_tokens']; + + Log::info('LlmDriver::Claude::completion'); + + /** + * I need to iterate over each item + * then if there are two rows with role assistant I need to insert + * in betwee a user row with some copy to make it work like "And the user search results had" + * using the Laravel Collection library + */ + $messages = collect($messages)->map(function ($item) { + if ($item->role === 'system') { + $item->role = 'assistant'; + } + + return $item->toArray(); + })->reverse()->values(); + + $messages = $messages->flatMap(function ($item, $index) use ($messages) { + if ($index > 0 && $item['role'] === 'assistant' && optional($messages->get($index + 1))['role'] === 'assistant') { + return [ + $item, + ['role' => 'user', 'content' => 'Continuation of search results'], + ]; + } + + return [$item]; + })->toArray(); + + put_fixture('claude_messages_debug.json', $messages); + + $results = $this->getClient()->post('/messages', [ + 'model' => $model, + 'system' => 'Return a markdown response.', + 'max_tokens' => $maxTokens, + 'messages' => $messages, + ]); + + if (! $results->ok()) { + $error = $results->json()['error']['type']; + $message = $results->json()['error']['message']; + Log::error('Claude API Error ', [ + 'type' => $error, + 'message' => $message, + ]); + throw new \Exception('Claude API Error '.$message); } - Log::info('LlmDriver::MockClient::completion'); + $data = null; - $data = <<<'EOD' - Voluptate irure cillum dolor anim officia reprehenderit dolor. Eiusmod veniam nostrud consectetur incididunt proident id. Anim adipisicing pariatur amet duis Lorem sunt veniam veniam est. Deserunt ea aliquip cillum pariatur consectetur. Dolor in reprehenderit adipisicing consectetur cupidatat ad cupidatat reprehenderit. Nostrud mollit voluptate aliqua anim pariatur excepteur eiusmod velit quis exercitation tempor quis excepteur. -EOD; + foreach ($results->json()['content'] as $content) { + $data = $content['text']; + } - return new CompletionResponse($data); + return CompletionResponse::from([ + 'content' => $data, + ]); } public function completion(string $prompt): CompletionResponse { - if (! app()->environment('testing')) { - sleep(2); + $model = $this->getConfig('claude')['models']['completion_model']; + $maxTokens = $this->getConfig('claude')['max_tokens']; + + Log::info('LlmDriver::Claude::completion'); + + $results = $this->getClient()->post('/messages', [ + 'model' => $model, + 'max_tokens' => $maxTokens, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $prompt, + ], + ], + ]); + + if (! $results->ok()) { + $error = $results->json()['error']['type']; + Log::error('Claude API Error '.$error); + throw new \Exception('Claude API Error '.$error); } - Log::info('LlmDriver::MockClient::completion'); + $data = null; + + foreach ($results->json()['content'] as $content) { + $data = $content['text']; + } - $data = <<<'EOD' - Voluptate irure cillum dolor anim officia reprehenderit dolor. Eiusmod veniam nostrud consectetur incididunt proident id. Anim adipisicing pariatur amet duis Lorem sunt veniam veniam est. Deserunt ea aliquip cillum pariatur consectetur. Dolor in reprehenderit adipisicing consectetur cupidatat ad cupidatat reprehenderit. Nostrud mollit voluptate aliqua anim pariatur excepteur eiusmod velit quis exercitation tempor quis excepteur. -EOD; + return CompletionResponse::from([ + 'content' => $data, + ]); + } + + protected function getError(Response $response) + { + return $response->json()['error']['type']; + } + + protected function getClient() + { + $api_token = $this->getConfig('claude')['api_key']; + if (! $api_token) { + throw new \Exception('Claude API Token not found'); + } - return new CompletionResponse($data); + return Http::withHeaders([ + 'x-api-key' => $api_token, + 'anthropic-version' => $this->version, + 'content-type' => 'application/json', + ])->baseUrl($this->baseUrl); } protected function getConfig(string $driver): array diff --git a/app/LlmDriver/DriversEnum.php b/app/LlmDriver/DriversEnum.php new file mode 100644 index 00000000..c69cba44 --- /dev/null +++ b/app/LlmDriver/DriversEnum.php @@ -0,0 +1,13 @@ + 'boolean', + 'driver' => DriversEnum::class, + 'embedding_driver' => DriversEnum::class, ]; public function team(): BelongsTo @@ -36,7 +42,12 @@ public function team(): BelongsTo public function getDriver(): string { - return $this->driver; + return $this->driver->value; + } + + public function getEmbeddingDriver(): string + { + return $this->embedding_driver->value; } public function documents(): HasMany diff --git a/app/Models/Document.php b/app/Models/Document.php index c436533b..9360a89f 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -4,6 +4,7 @@ use App\Domains\Documents\StatusEnum; use App\Domains\Documents\TypesEnum; +use App\LlmDriver\HasDrivers; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -17,7 +18,7 @@ * @property string|null $summary * @property string|null $file_path */ -class Document extends Model +class Document extends Model implements HasDrivers { use HasFactory; @@ -58,6 +59,11 @@ public function mkdirPathToFile(): ?string public function getDriver(): string { - return $this->collection->driver; + return $this->collection->driver->value; + } + + public function getEmbeddingDriver(): string + { + return $this->collection->embedding_driver->value; } } diff --git a/app/Models/DocumentChunk.php b/app/Models/DocumentChunk.php index c9b9d2d7..5c819919 100644 --- a/app/Models/DocumentChunk.php +++ b/app/Models/DocumentChunk.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Domains\Documents\StatusEnum; +use App\LlmDriver\HasDrivers; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Pgvector\Laravel\Vector; @@ -10,7 +11,7 @@ /** * @property Document $document */ -class DocumentChunk extends Model +class DocumentChunk extends Model implements HasDrivers { use HasFactory; @@ -41,8 +42,13 @@ protected static function booted() }); } + public function getEmbeddingDriver(): string + { + return $this->document->collection->embedding_driver->value; + } + public function getDriver(): string { - return $this->document->collection->driver; + return $this->document->collection->driver->value; } } diff --git a/app/helpers.php b/app/helpers.php index e5501a99..303c9bed 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -25,7 +25,7 @@ function put_fixture($file_name, $content = [], $json = true) if (! function_exists('remove_ascii')) { function remove_ascii($string): string { - return preg_replace('/[^\x00-\x7F]+/', '', $string); + return str_replace("\u2019", ' ', preg_replace('/[^\x00-\x7F]+/', '', $string)); } } diff --git a/config/llmdriver.php b/config/llmdriver.php index 5c090d18..320e0407 100644 --- a/config/llmdriver.php +++ b/config/llmdriver.php @@ -14,8 +14,16 @@ 'completion_model' => env('OPENAI_COMPLETION_MODEL', 'gpt-4-turbo-preview'), 'chat_model' => env('OPENAICHAT_MODEL', 'gpt-4-turbo-preview'), ], - 'azure' => [ + 'mock' => [ + ], + 'claude' => [ + 'api_key' => env('CLAUDE_API_KEY'), + 'max_tokens' => env('CLAUDE_MAX_TOKENS', 4000), + 'models' => [ + //@see https://www.anthropic.com/news/claude-3-family + 'completion_model' => env('CLAUDE_COMPLETION_MODEL', 'claude-3-opus-20240229'), + ], ], 'ollama' => [ diff --git a/database/factories/CollectionFactory.php b/database/factories/CollectionFactory.php index ec0de092..498246a2 100644 --- a/database/factories/CollectionFactory.php +++ b/database/factories/CollectionFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\LlmDriver\DriversEnum; use App\Models\Team; use Illuminate\Database\Eloquent\Factories\Factory; @@ -22,6 +23,8 @@ public function definition(): array 'description' => $this->faker->paragraph, 'active' => $this->faker->boolean, 'team_id' => Team::factory(), + 'driver' => DriversEnum::Mock, + 'embedding_driver' => DriversEnum::Mock, ]; } } diff --git a/database/migrations/2024_03_29_200402_add_embedding_driver_to_collections_model.php b/database/migrations/2024_03_29_200402_add_embedding_driver_to_collections_model.php new file mode 100644 index 00000000..6de31dc0 --- /dev/null +++ b/database/migrations/2024_03_29_200402_add_embedding_driver_to_collections_model.php @@ -0,0 +1,29 @@ +string('embedding_driver')->default(DriversEnum::Mock); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('collections', function (Blueprint $table) { + // + }); + } +}; diff --git a/phpunit.xml b/phpunit.xml index e90e0b93..236e4e5a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -32,5 +32,6 @@ + diff --git a/resources/js/Pages/Collection/Components/EmbeddingType.vue b/resources/js/Pages/Collection/Components/EmbeddingType.vue new file mode 100644 index 00000000..f8239bb6 --- /dev/null +++ b/resources/js/Pages/Collection/Components/EmbeddingType.vue @@ -0,0 +1,66 @@ + + + \ No newline at end of file diff --git a/resources/js/Pages/Collection/Components/LlmType.vue b/resources/js/Pages/Collection/Components/LlmType.vue index da86f36e..2ad45dfa 100644 --- a/resources/js/Pages/Collection/Components/LlmType.vue +++ b/resources/js/Pages/Collection/Components/LlmType.vue @@ -1,6 +1,5 @@