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 bd5df8e8..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->getEmddingDriver() + $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/LlmDriver/ClaudeClient.php b/app/LlmDriver/ClaudeClient.php index 5b42416d..042208dc 100644 --- a/app/LlmDriver/ClaudeClient.php +++ b/app/LlmDriver/ClaudeClient.php @@ -11,7 +11,6 @@ class ClaudeClient { - protected string $baseUrl = 'https://api.anthropic.com/v1'; protected string $version = '2023-06-01'; @@ -20,13 +19,7 @@ class ClaudeClient public function embedData(string $data): EmbeddingsResponseDto { - - Log::info('LlmDriver::ClaudeClient::embedData'); - - return EmbeddingsResponseDto::from([ - 'embedding' => data_get($data, 'data.0.embedding'), - 'token_count' => 1000, - ]); + throw new \Exception('Not implemented'); } /** @@ -34,34 +27,58 @@ public function embedData(string $data): EmbeddingsResponseDto */ public function chat(array $messages): CompletionResponse { - $model = $this->getConfig('claude')['models']['completion_model']; - $maxTokens = $this->getConfig('claude')['max_tokens']; + $model = $this->getConfig('claude')['models']['completion_model']; + $maxTokens = $this->getConfig('claude')['max_tokens']; Log::info('LlmDriver::Claude::completion'); - $messages = collect($messages)->map(function($item) { - if($item->role === 'system') { + /** + * 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()->all(); - + })->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, - "max_tokens" => $maxTokens, + 'system' => 'Return a markdown response.', + 'max_tokens' => $maxTokens, 'messages' => $messages, ]); - if(!$results->ok()) { + if (! $results->ok()) { $error = $results->json()['error']['type']; - Log::error('Claude API Error ' . $error); - throw new \Exception('Claude API Error ' . $error); + $message = $results->json()['error']['message']; + Log::error('Claude API Error ', [ + 'type' => $error, + 'message' => $message, + ]); + throw new \Exception('Claude API Error '.$message); } $data = null; - foreach($results->json()['content'] as $content) { + foreach ($results->json()['content'] as $content) { $data = $content['text']; } @@ -72,32 +89,31 @@ public function chat(array $messages): CompletionResponse public function completion(string $prompt): CompletionResponse { - $model = $this->getConfig('claude')['models']['completion_model']; - $maxTokens = $this->getConfig('claude')['max_tokens']; + $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, + 'max_tokens' => $maxTokens, 'messages' => [ [ - 'role' => "user", - 'content' => $prompt - ] + 'role' => 'user', + 'content' => $prompt, + ], ], ]); - - if(!$results->ok()) { + if (! $results->ok()) { $error = $results->json()['error']['type']; - Log::error('Claude API Error ' . $error); - throw new \Exception('Claude API Error ' . $error); + Log::error('Claude API Error '.$error); + throw new \Exception('Claude API Error '.$error); } $data = null; - foreach($results->json()['content'] as $content) { + foreach ($results->json()['content'] as $content) { $data = $content['text']; } @@ -106,13 +122,15 @@ public function completion(string $prompt): CompletionResponse ]); } - protected function getError(Response $response) { + protected function getError(Response $response) + { return $response->json()['error']['type']; } - protected function getClient() { + protected function getClient() + { $api_token = $this->getConfig('claude')['api_key']; - if(!$api_token) { + if (! $api_token) { throw new \Exception('Claude API Token not found'); } diff --git a/app/LlmDriver/DriversEnum.php b/app/LlmDriver/DriversEnum.php index 0ce69dec..c69cba44 100644 --- a/app/LlmDriver/DriversEnum.php +++ b/app/LlmDriver/DriversEnum.php @@ -1,13 +1,13 @@ - 'boolean', 'driver' => DriversEnum::class, - 'embedding_driver' => DriversEnum::class + 'embedding_driver' => DriversEnum::class, ]; - public function team(): BelongsTo { return $this->belongsTo(Team::class); diff --git a/app/Models/Document.php b/app/Models/Document.php index 3fac048c..9360a89f 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -30,7 +30,6 @@ class Document extends Model implements HasDrivers 'summary_status' => StatusEnum::class, ]; - public function collection(): BelongsTo { return $this->belongsTo(Collection::class); @@ -63,8 +62,8 @@ public function getDriver(): string return $this->collection->driver->value; } - - public function getEmbeddingDriver(): string { + public function getEmbeddingDriver(): string + { return $this->collection->embedding_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 e56c26e7..320e0407 100644 --- a/config/llmdriver.php +++ b/config/llmdriver.php @@ -23,7 +23,7 @@ 'models' => [ //@see https://www.anthropic.com/news/claude-3-family 'completion_model' => env('CLAUDE_COMPLETION_MODEL', 'claude-3-opus-20240229'), - ] + ], ], 'ollama' => [ diff --git a/tests/Feature/ClaudeClientTest.php b/tests/Feature/ClaudeClientTest.php index e7b6de77..8f4e6330 100644 --- a/tests/Feature/ClaudeClientTest.php +++ b/tests/Feature/ClaudeClientTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature; use App\LlmDriver\ClaudeClient; -use App\LlmDriver\MockClient; use App\LlmDriver\Requests\MessageInDto; use App\LlmDriver\Responses\CompletionResponse; use App\LlmDriver\Responses\EmbeddingsResponseDto; @@ -18,6 +17,8 @@ class ClaudeClientTest extends TestCase public function test_embeddings(): void { + $this->markTestSkipped('@TODO: Requires another server'); + $client = new ClaudeClient(); $results = $client->embedData('test'); @@ -68,9 +69,52 @@ public function test_chat(): void Http::assertSent(function ($request) { $message1 = $request->data()['messages'][0]['role']; $message2 = $request->data()['messages'][1]['role']; + return $message2 === 'assistant' && $message1 === 'user'; }); } + + public function test_chat_with_multiple_assistant_messages(): void + { + $client = new ClaudeClient(); + + $data = get_fixture('claude_completion.json'); + + Http::fake([ + 'api.anthropic.com/*' => Http::response($data, 200), + ]); + + $results = $client->chat([ + MessageInDto::from([ + 'content' => 'test 3', + 'role' => 'assistant', + ]), + MessageInDto::from([ + 'content' => 'test 2', + 'role' => 'assistant', + ]), + MessageInDto::from([ + 'content' => 'test 1', + 'role' => 'assistant', + ]), + MessageInDto::from([ + 'content' => 'test', + 'role' => 'user', + ]), + ]); + + $this->assertInstanceOf(CompletionResponse::class, $results); + + Http::assertSent(function ($request) { + $message1 = $request->data()['messages'][0]['role']; + $message2 = $request->data()['messages'][1]['role']; + $message3 = $request->data()['messages'][1]['role']; + + return $message2 === 'assistant' && + $message1 === 'user' && $message3 === 'assistant'; + }); + + } } diff --git a/tests/Feature/Http/Controllers/CollectionControllerTest.php b/tests/Feature/Http/Controllers/CollectionControllerTest.php index eaf425dd..c99ef50a 100644 --- a/tests/Feature/Http/Controllers/CollectionControllerTest.php +++ b/tests/Feature/Http/Controllers/CollectionControllerTest.php @@ -10,7 +10,6 @@ use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Storage; use Inertia\Testing\AssertableInertia as Assert; -use Laravel\Pennant\Contracts\Driver; use Tests\TestCase; class CollectionControllerTest extends TestCase diff --git a/tests/fixtures/claude_messages_debug.json b/tests/fixtures/claude_messages_debug.json new file mode 100644 index 00000000..1c773dcf --- /dev/null +++ b/tests/fixtures/claude_messages_debug.json @@ -0,0 +1,26 @@ +[ + { + "content": "test", + "role": "user" + }, + { + "content": "test 1", + "role": "assistant" + }, + { + "role": "user", + "content": "Continuation of search results" + }, + { + "content": "test 2", + "role": "assistant" + }, + { + "role": "user", + "content": "Continuation of search results" + }, + { + "content": "test 3", + "role": "assistant" + } +] \ No newline at end of file