From 5a74339aef66c2671d252f009f09190d251c115d Mon Sep 17 00:00:00 2001 From: Alfred Nutile Date: Wed, 27 Mar 2024 10:45:43 -0400 Subject: [PATCH 1/7] ok now to try the chat --- app/Domains/Messages/RoleEnum.php | 10 ++ app/Http/Controllers/ChatController.php | 28 ++- app/Http/Resources/ChatResource.php | 5 +- app/Http/Resources/MessageResource.php | 19 ++ app/Http/Resources/UserResource.php | 19 ++ app/LlmDriver/BaseClient.php | 11 ++ app/Models/Chat.php | 57 ++++-- app/Models/Collection.php | 16 ++ app/Models/Message.php | 6 + config/llmdriver.php | 1 + config/llmlarahub.php | 8 + database/factories/MessageFactory.php | 2 + ...2024_03_27_132847_add_role_to_messages.php | 28 +++ resources/js/Pages/Chat/ChatInputThreaded.vue | 69 ++----- resources/js/Pages/Chat/ChatMessage.vue | 63 +------ resources/js/Pages/Chat/ChatUi.vue | 170 +----------------- resources/js/Pages/Collection/Chat.vue | 17 +- routes/web.php | 2 + .../Http/Controllers/ChatControllerTest.php | 22 +++ tests/Feature/Models/ChatTest.php | 22 +++ tests/Feature/Models/CollectionTest.php | 13 ++ tests/fixtures/chat_messages.json | 10 ++ 22 files changed, 293 insertions(+), 305 deletions(-) create mode 100644 app/Domains/Messages/RoleEnum.php create mode 100644 app/Http/Resources/MessageResource.php create mode 100644 app/Http/Resources/UserResource.php create mode 100644 config/llmlarahub.php create mode 100644 database/migrations/2024_03_27_132847_add_role_to_messages.php create mode 100644 tests/fixtures/chat_messages.json diff --git a/app/Domains/Messages/RoleEnum.php b/app/Domains/Messages/RoleEnum.php new file mode 100644 index 00000000..becb1a25 --- /dev/null +++ b/app/Domains/Messages/RoleEnum.php @@ -0,0 +1,10 @@ + new CollectionResource($collection), 'chat' => new ChatResource($chat), + 'system_prompt' => $collection->systemPrompt(), + 'messages' => MessageResource::collection($chat->messages), + ]); + } + + public function chat(Chat $chat) + { + $validated = request()->validate([ + 'input' => 'required|string', ]); + $chat->addInput($validated['input'], RoleEnum::User, $chat->chatable->systemPrompt()); + + $latestMessagesArray = $chat->getChatResponse(); + + put_fixture('chat_messages.json', $latestMessagesArray); + + /** @var CompletionResponse $response */ + $response = LlmDriverFacade::driver( + $chat->chatable->getDriver() + )->chat($latestMessagesArray); + + $chat->addInput($response->content, RoleEnum::Assistant); + + return response()->json(['message' => $response->content]); } } diff --git a/app/Http/Resources/ChatResource.php b/app/Http/Resources/ChatResource.php index a15090ac..f190fd66 100644 --- a/app/Http/Resources/ChatResource.php +++ b/app/Http/Resources/ChatResource.php @@ -14,6 +14,9 @@ class ChatResource extends JsonResource */ public function toArray(Request $request): array { - return parent::toArray($request); + return [ + 'id' => $this->id, + 'user_id' => new UserResource($this->user), + ]; } } diff --git a/app/Http/Resources/MessageResource.php b/app/Http/Resources/MessageResource.php new file mode 100644 index 00000000..848ea85e --- /dev/null +++ b/app/Http/Resources/MessageResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 00000000..3b6b127a --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/LlmDriver/BaseClient.php b/app/LlmDriver/BaseClient.php index 35308ea7..a0e43673 100644 --- a/app/LlmDriver/BaseClient.php +++ b/app/LlmDriver/BaseClient.php @@ -23,6 +23,17 @@ public function embedData(string $data): EmbeddingsResponseDto ); } + public function chat(array $messages): CompletionResponse + { + Log::info('LlmDriver::MockClient::completion'); + + $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 new CompletionResponse($data); + } + public function completion(string $prompt): CompletionResponse { Log::info('LlmDriver::MockClient::completion'); diff --git a/app/Models/Chat.php b/app/Models/Chat.php index a3afc6e0..9e8d3ca3 100644 --- a/app/Models/Chat.php +++ b/app/Models/Chat.php @@ -2,18 +2,41 @@ namespace App\Models; +use App\Domains\Messages\RoleEnum; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; use OpenAI\Laravel\Facades\OpenAI; + +/** + * @property mixed $chatable; + * @package App\Models + */ class Chat extends Model { use HasFactory; protected $fillable = []; + protected function createSystemMessageIfNeeded(string $systemPrompt): void + { + if ($this->messages()->count() == 0) { + $this->messages()->create( + [ + 'body' => $systemPrompt, + 'in_out' => false, + 'role' => RoleEnum::System, + 'created_at' => now(), + 'updated_at' => now(), + 'chat_id' => $this->id, + 'is_chat_ignored' => false, + ]); + } + } + /* ----------------------------------------------------------------- | Methods | ----------------------------------------------------------------- @@ -21,17 +44,24 @@ class Chat extends Model /** * Save the input message of the user */ - public function addInput(string $message, bool $in = true, bool $isChatIgnored = false): Message + public function addInput(string $message, + RoleEnum $role = RoleEnum::User, + ?string $systemPrompt = null): Message { + if ($systemPrompt) { + $this->createSystemMessageIfNeeded($systemPrompt); + } + $message = $this->messages()->create( [ 'body' => $message, - 'in_out' => $in, + 'role' => $role, + 'in_out' => ($role === RoleEnum::User) ? true : false, 'created_at' => now(), 'updated_at' => now(), 'chat_id' => $this->id, - 'is_chat_ignored' => $isChatIgnored, + 'is_chat_ignored' => false, ]); return $message; @@ -44,7 +74,7 @@ public function addOutput(string $request): Message { $message = $this->getAiResponse($request); - return $this->addInput($message, false); + return $this->addInput($message, RoleEnum::Assistant); } /** @@ -60,28 +90,21 @@ public function getAiResponse($input) return $this->getOpenAiImage(); } - return $this->getOpenAiChat(); + return $this->getChatResponse(); } - /** - * Get response chat from OpenAI - */ - public function getOpenAiChat(int $limit = 5): string + public function getChatResponse(int $limit = 5): array { $latestMessages = $this->messages()->latest()->limit($limit)->get()->sortBy('id'); - /** - * Reverse the messages to preserve the order for OpenAI - */ $latestMessagesArray = []; + foreach ($latestMessages as $message) { $latestMessagesArray[] = [ - 'role' => $message->in_out ? 'user' : 'assistant', 'content' => $message->compressed_body]; + 'role' => $message->role, 'content' => $message->compressed_body]; } - $response = OpenAI::chat()->create(['model' => 'gpt-3.5-turbo', 'messages' => $latestMessagesArray]); - - return $response->choices[0]->message->content; + return $latestMessagesArray; } @@ -96,7 +119,7 @@ public function getOpenAiImage(): string return ''; } - public function chatable() + public function chatable() : MorphTo { return $this->morphTo(); } diff --git a/app/Models/Collection.php b/app/Models/Collection.php index 87dbbc7e..d2a7aa4b 100644 --- a/app/Models/Collection.php +++ b/app/Models/Collection.php @@ -34,6 +34,11 @@ public function team(): BelongsTo return $this->belongsTo(Team::class); } + public function getDriver(): string + { + return $this->driver; + } + public function documents(): HasMany { return $this->hasMany(Document::class); @@ -43,4 +48,15 @@ public function chats(): MorphMany { return $this->morphMany(Chat::class, 'chatable'); } + + public function systemPrompt(): string + { + $systemPrompt = config('llmlarahub.collection.system_prompt'); + $prompt = <<description} +EOD; + + return $prompt; + } } diff --git a/app/Models/Message.php b/app/Models/Message.php index 7f4ab54e..8857f935 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Domains\Messages\RoleEnum; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -15,6 +16,11 @@ class Message extends Model 'in_out', ]; + protected $casts = [ + 'role' => RoleEnum::class, + 'in_out' => 'boolean', + ]; + /** * Return true if the message is from the user. */ diff --git a/config/llmdriver.php b/config/llmdriver.php index a6f0c1f6..5c090d18 100644 --- a/config/llmdriver.php +++ b/config/llmdriver.php @@ -12,6 +12,7 @@ 'api_url' => env('OPENAI_API_URL', 'https://api.openai.com/v1/engines/davinci-codex/completions'), 'embedding_model' => env('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-large'), 'completion_model' => env('OPENAI_COMPLETION_MODEL', 'gpt-4-turbo-preview'), + 'chat_model' => env('OPENAICHAT_MODEL', 'gpt-4-turbo-preview'), ], 'azure' => [ diff --git a/config/llmlarahub.php b/config/llmlarahub.php new file mode 100644 index 00000000..3509c552 --- /dev/null +++ b/config/llmlarahub.php @@ -0,0 +1,8 @@ + [ + 'system_prompt' => 'This is a collection of data the user has imported that they will + ask questions about. The description they gave for this collection is', + ], +]; diff --git a/database/factories/MessageFactory.php b/database/factories/MessageFactory.php index 7d7547be..60e22bb2 100644 --- a/database/factories/MessageFactory.php +++ b/database/factories/MessageFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Domains\Messages\RoleEnum; use App\Models\Chat; use Illuminate\Database\Eloquent\Factories\Factory; @@ -20,6 +21,7 @@ public function definition(): array return [ 'body' => $this->faker->paragraphs(3, true), 'in_out' => $this->faker->boolean, + 'role' => RoleEnum::User, 'chat_id' => Chat::factory(), ]; } diff --git a/database/migrations/2024_03_27_132847_add_role_to_messages.php b/database/migrations/2024_03_27_132847_add_role_to_messages.php new file mode 100644 index 00000000..153d5ab0 --- /dev/null +++ b/database/migrations/2024_03_27_132847_add_role_to_messages.php @@ -0,0 +1,28 @@ +string('role')->default('user'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('messages', function (Blueprint $table) { + // + }); + } +}; diff --git a/resources/js/Pages/Chat/ChatInputThreaded.vue b/resources/js/Pages/Chat/ChatInputThreaded.vue index e3e8a088..54450323 100644 --- a/resources/js/Pages/Chat/ChatInputThreaded.vue +++ b/resources/js/Pages/Chat/ChatInputThreaded.vue @@ -7,12 +7,13 @@ import {computed, inject, onUnmounted, ref, watch} from "vue"; const props = defineProps({ loading: { type: Boolean, - default: true + default: false }, - assistant: Object, chat: Object, }) +const system_prompt = inject('system_prompt') + const emits = defineEmits(['chatSubmitted']) const errors = ref({}) @@ -24,54 +25,8 @@ const form = useForm({ const getting_results = ref(false) -let echoChannel = ref(null); // keep track of the Echo channel - -watch(() => props.chat?.id, (newVal, oldVal) => { - if (newVal !== undefined && newVal !== oldVal) { // check if the id has a value and it's different from the previous one - if(props.chat?.id) { - echoChannel.value = Echo.private(`chat.user.${usePage().props.user.id}`) - .listen('.complete', (event) => { - getting_results.value = false; - }); - } else { - console.log("No chat id yet") - } - - } -}, { immediate: true }); // { immediate: true } ensures that the watcher checks the initial value - -onUnmounted(() => { - if(echoChannel.value) { - echoChannel.value.stopListening('.stream'); // stop listening when component is unmounted - } -}); - -const starterQuestions = computed(() => { - if(!props.chat?.id) { - return usePage().props.assistants.default.starter_questions; - } - - return usePage().props.assistants[props.chat.assistant.assistant_type]['starter_questions']; -}) - - const save = () => { getting_results.value = true - form.post(route('assistants.converse', { - assistant: props.assistant.id - }), { - onSuccess: (data) => { - console.log(data) - emits('chatSubmitted', form.input) - form.reset(); - }, - onError: (error) => { - console.log("Error") - console.log(error) - errors.value = error - } - }) - } const setQuestion = (question) => { @@ -91,19 +46,19 @@ const setQuestion = (question) => { - + + - + - -
- -
diff --git a/resources/js/Pages/Chat/ChatMessage.vue b/resources/js/Pages/Chat/ChatMessage.vue index ecc266e4..f4eb4d5f 100644 --- a/resources/js/Pages/Chat/ChatMessage.vue +++ b/resources/js/Pages/Chat/ChatMessage.vue @@ -10,67 +10,12 @@ const props = defineProps({ type: Boolean, default: true }, - assistant: Object, + messages: Object, chat: Object, }) const toast = useToast(); -const chatType = ref('threaded') - -const eventSource = ref({}) - -const waiting_on_run = ref(false) - -const eventData = ref("") - - -const messages = ref([]) - -const getMessages = () => { - axios.get(route("assistants.messages", { - assistant: props.assistant.id - })).then(data => { - console.log(data.data); - messages.value = data.data.messages; - }).catch(error => { - console.log("Error getting messages") - console.log(error) - }) -} - -const messagesComputed = computed(() => { - if(messages.value.length === 0) { - return props.chat?.messages; - } - - return messages.value; -}) - -let echoChannel = ref(null); // keep track of the Echo channel - -watch(() => props.chat?.id, (newVal, oldVal) => { - if (newVal !== undefined && newVal !== oldVal) { // check if the id has a value and it's different from the previous one - if(props.chat?.id) { - echoChannel.value = Echo.private(`chat.user.${usePage().props.user.id}`) - .listen('.threaded', (event) => { - getMessages(); - }).listen('.complete', (event) => { - getMessages(); - }); - } else { - console.log("No chat id yet") - } - - } -}, { immediate: true }); // { immediate: true } ensures that the watcher checks the initial value - -onUnmounted(() => { - if(echoChannel.value) { - echoChannel.value.stopListening('.stream'); // stop listening when component is unmounted - } -}); - @@ -78,11 +23,11 @@ onUnmounted(() => {
-
- Start: {{ chat.title }} +
+ Start:
-
+
diff --git a/resources/js/Pages/Chat/ChatUi.vue b/resources/js/Pages/Chat/ChatUi.vue index 30617b74..2822f155 100644 --- a/resources/js/Pages/Chat/ChatUi.vue +++ b/resources/js/Pages/Chat/ChatUi.vue @@ -8,182 +8,24 @@ import {router, useForm, usePage} from "@inertiajs/vue3"; const toast = useToast(); const props = defineProps({ - chats: Object, - copy: Object, + chat: Object, + messages: Object, }) -const chatType = ref('threaded') -const chat = ref({}) -const assistant = ref({}) -const messages = ref([]) -const loading_assistant = ref(false) -const assistant_setup_running = ref(false) - - -provide('chat', chat.value) -provide('assistant', assistant.value) -provide('messages', messages.value) - -Echo.private(`chat.user.${usePage().props.user.id}`) - .listen('.status', (event) => { - if(event.assistant.setup_status !== 'completed') { - toast("Assistant status " + event.assistant.setup_status) - loading_message.value = runningMessage(); - } - }).listen('.complete', (event) => { - assistant_setup_running.value = false; -}); - -const instantiateAssistant = () => { - loading_assistant.value = true; - - messages.value = []; - chat.value = {}; - assistant.value = {}; - - axios.post(route('assistants.make'), - { - assistant_type: assistantType.value - }).then(data => { - console.log(data) - assistant.value = data.data.assistant; - chat.value = data.data.chat; - messages.value = data.data.chat.messages; - loading_assistant.value = false; - - - if(assistant.value.setup_status === 'completed' || assistant.value.setup_status === 'default') { - assistant_setup_running.value = false; - } else { - assistant_setup_running.value = true; - } - - router.reload(); - - - }).catch(error => { - console.log(error) - toast.error("Error getting assistant please see logs") - loading_assistant.value = false; - }) -} - -const runningMessage = () => { - let messages = [ - 'If this is the first chat it might take a minute to set things up', - ]; - // Generating a random index - let randomIndex = Math.floor(Math.random() * messages.length); - - // Returning the message at the random index - return messages[randomIndex]; -} - -const loading_message = ref(runningMessage()) - - -const waiting_on_run = ref(false) - -const reloadMessages = () => { - waiting_on_run.value = true; - router.reload(); -} - -const assistantType = ref("") - -const makeAssistant = (type) => { - console.log(type) - assistantType.value = type; - instantiateAssistant(); -} - -const previousChat = ref({}) - -const choosePreviousChat = () => { - chat.value = previousChat.value; - assistant.value = previousChat.value.assistant; - messages.value = previousChat.messages; - loading_assistant.value = false; -} diff --git a/resources/js/Pages/Collection/Chat.vue b/resources/js/Pages/Collection/Chat.vue index 60cdb330..62637219 100644 --- a/resources/js/Pages/Collection/Chat.vue +++ b/resources/js/Pages/Collection/Chat.vue @@ -2,23 +2,30 @@ import AppLayout from '@/Layouts/AppLayout.vue'; import Welcome from '@/Components/Welcome.vue'; import PrimaryButton from '@/Components/PrimaryButton.vue'; -import { computed, onMounted, ref } from 'vue'; +import { computed, onMounted, provide, ref } from 'vue'; import { useDropzone } from "vue3-dropzone"; import { router, useForm } from '@inertiajs/vue3'; import FileUploader from './Components/FileUploader.vue'; - +import ChatUi from '@/Pages/Chat/ChatUi.vue'; const props = defineProps({ collection: { type: Object, required: true, }, + system_prompt: { + type: String, + required: true, + }, chat: { type: Object, }, + messages: { + type: Object, + }, }); - +provide('system_prompt', props.system_prompt); @@ -49,7 +56,9 @@ const props = defineProps({

- Chat widget here +
diff --git a/routes/web.php b/routes/web.php index 33e2dc11..a79e4ba9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -42,6 +42,8 @@ Route::controller(ChatController::class)->group(function () { Route::post('/collections/{collection}/chats', 'storeCollectionChat')->name('chats.collection.store'); Route::get('/collections/{collection}/chats/{chat}', 'showCollectionChat')->name('chats.collection.show'); + Route::post('/chats/{chat}/messages/create', 'chat') + ->name('chats.messages.create'); }); }); diff --git a/tests/Feature/Http/Controllers/ChatControllerTest.php b/tests/Feature/Http/Controllers/ChatControllerTest.php index 3dde1f67..74153557 100644 --- a/tests/Feature/Http/Controllers/ChatControllerTest.php +++ b/tests/Feature/Http/Controllers/ChatControllerTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Http\Controllers; +use App\Models\Chat; use App\Models\Collection; use App\Models\User; use Tests\TestCase; @@ -22,4 +23,25 @@ public function test_can_create_chat_and_redirect(): void ]))->assertRedirect(); $this->assertDatabaseCount('chats', 1); } + + public function test_kick_off_chat_makes_system() + { + $user = User::factory()->create(); + $collection = Collection::factory()->create(); + $chat = Chat::factory()->create([ + 'chatable_id' => $collection->id, + 'chatable_type' => Collection::class, + 'user_id' => $user->id, + ]); + + $this->assertDatabaseCount('messages', 0); + $this->actingAs($user)->post(route('chats.messages.create', [ + 'chat' => $chat->id, + ]), + [ + 'system_prompt' => 'Foo', + 'input' => 'user input', + ])->assertOk(); + $this->assertDatabaseCount('messages', 3); + } } diff --git a/tests/Feature/Models/ChatTest.php b/tests/Feature/Models/ChatTest.php index c2a9d44e..624bbf78 100644 --- a/tests/Feature/Models/ChatTest.php +++ b/tests/Feature/Models/ChatTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Models; +use App\Domains\Messages\RoleEnum; use App\Models\Chat; use App\Models\Collection; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -24,6 +25,27 @@ public function test_factory(): void $this->assertNotNull($model->user->id); $this->assertNotNull($model->chatable_id); $this->assertNotNull($model->chatable->id); + $this->assertNotNull($model->chatable->systemPrompt()); $this->assertNotNull($collection->chats()->first()->id); } + + public function test_system_message(): void + { + $collection = Collection::factory()->create(); + $chat = Chat::factory()->create([ + 'chatable_id' => $collection->id, + ]); + + $this->assertDatabaseCount('messages', 0); + $chat->addInput( + message: 'Test', + role: RoleEnum::User, + systemPrompt: 'Hello' + ); + $this->assertDatabaseCount('messages', 2); + $chat->addInput( + message: 'Test', + role: RoleEnum::User); + $this->assertDatabaseCount('messages', 3); + } } diff --git a/tests/Feature/Models/CollectionTest.php b/tests/Feature/Models/CollectionTest.php index eb904adc..d2ca9cc1 100644 --- a/tests/Feature/Models/CollectionTest.php +++ b/tests/Feature/Models/CollectionTest.php @@ -16,4 +16,17 @@ public function test_factory(): void $this->assertNotNull($model->team->id); } + + public function test_system_prompt(): void + { + $model = \App\Models\Collection::factory()->create(); + + $this->assertStringContainsString( + config('llmlarahub.collection.system_prompt'), + $model->systemPrompt()); + + $this->assertStringContainsString( + $model->description, + $model->systemPrompt()); + } } diff --git a/tests/fixtures/chat_messages.json b/tests/fixtures/chat_messages.json new file mode 100644 index 00000000..ee5d8c46 --- /dev/null +++ b/tests/fixtures/chat_messages.json @@ -0,0 +1,10 @@ +[ + { + "role": "user", + "content": "This is a collection of data the user has imported that they will \n ask questions about. The description they gave for this collection is: \nEt quas quis totam. Ipsum rerum et corporis. Recusandae aut omnis pariatur veritatis id." + }, + { + "role": "user", + "content": "user input" + } +] \ No newline at end of file From 639eb5a416934593621a47570b2521c2d8f95a34 Mon Sep 17 00:00:00 2001 From: Alfred Nutile Date: Wed, 27 Mar 2024 11:25:16 -0400 Subject: [PATCH 2/7] ui works with mock now to send more data to the system --- app/Http/Controllers/ChatController.php | 2 +- app/Http/Resources/MessageResource.php | 10 ++- app/LlmDriver/BaseClient.php | 4 ++ app/Models/Chat.php | 11 ++-- app/Models/Message.php | 6 +- package-lock.json | 6 ++ package.json | 1 + resources/js/Pages/Chat/ChatBaloon.vue | 13 +--- resources/js/Pages/Chat/ChatInputThreaded.vue | 61 +++++++++++++------ resources/js/Pages/Chat/ChatMessage.vue | 2 +- resources/js/Pages/Chat/ChatUi.vue | 18 +++--- resources/js/app.js | 4 ++ .../Http/Controllers/ChatControllerTest.php | 4 ++ tests/fixtures/chat_messages.json | 16 ++++- tests/fixtures/system_prompt.txt | 3 + 15 files changed, 111 insertions(+), 50 deletions(-) create mode 100644 tests/fixtures/system_prompt.txt diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index df015d46..9ef0ec2c 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -35,7 +35,7 @@ public function showCollectionChat(Collection $collection, Chat $chat) 'collection' => new CollectionResource($collection), 'chat' => new ChatResource($chat), 'system_prompt' => $collection->systemPrompt(), - 'messages' => MessageResource::collection($chat->messages), + 'messages' => MessageResource::collection($chat->latest_messages), ]); } diff --git a/app/Http/Resources/MessageResource.php b/app/Http/Resources/MessageResource.php index 848ea85e..12faeb82 100644 --- a/app/Http/Resources/MessageResource.php +++ b/app/Http/Resources/MessageResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -14,6 +15,13 @@ class MessageResource extends JsonResource */ public function toArray(Request $request): array { - return parent::toArray($request); + return [ + 'id' => $this->id, + 'from_ai' => $this->from_ai, + 'initials' => ($this->from_ai) ? "Ai" : "You", + 'type' => 'text', //@TODO + 'body' => $this->body, + 'diff_for_humans' => $this->created_at->diffForHumans(), + ]; } } diff --git a/app/LlmDriver/BaseClient.php b/app/LlmDriver/BaseClient.php index a0e43673..eae8a78c 100644 --- a/app/LlmDriver/BaseClient.php +++ b/app/LlmDriver/BaseClient.php @@ -25,6 +25,10 @@ public function embedData(string $data): EmbeddingsResponseDto public function chat(array $messages): CompletionResponse { + if(!app()->environment('testing')) { + sleep(3); + } + Log::info('LlmDriver::MockClient::completion'); $data = <<<'EOD' diff --git a/app/Models/Chat.php b/app/Models/Chat.php index 9e8d3ca3..b0c01aba 100644 --- a/app/Models/Chat.php +++ b/app/Models/Chat.php @@ -24,6 +24,7 @@ class Chat extends Model protected function createSystemMessageIfNeeded(string $systemPrompt): void { if ($this->messages()->count() == 0) { + $this->messages()->create( [ 'body' => $systemPrompt, @@ -32,7 +33,7 @@ protected function createSystemMessageIfNeeded(string $systemPrompt): void 'created_at' => now(), 'updated_at' => now(), 'chat_id' => $this->id, - 'is_chat_ignored' => false, + 'is_chat_ignored' => true, ]); } } @@ -124,10 +125,10 @@ public function chatable() : MorphTo return $this->morphTo(); } - /* ----------------------------------------------------------------- - | Relationships - | ----------------------------------------------------------------- - */ + public function latest_messages(): HasMany + { + return $this->hasMany(Message::class)->where("is_chat_ignored", false)->oldest(); + } /** * Chat has many messages. diff --git a/app/Models/Message.php b/app/Models/Message.php index 8857f935..9c7d0274 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -13,7 +13,9 @@ class Message extends Model public $fillable = [ 'body', + 'role', 'in_out', + 'is_chat_ignored', ]; protected $casts = [ @@ -26,7 +28,7 @@ class Message extends Model */ public function getFromUserAttribute(): bool { - return $this->in_out == true; + return $this->role === RoleEnum::User; } /** @@ -34,7 +36,7 @@ public function getFromUserAttribute(): bool */ public function getFromAiAttribute(): bool { - return ! $this->from_user; + return $this->role !== RoleEnum::User; } /** diff --git a/package-lock.json b/package-lock.json index cf745760..77ca2e8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@formkit/auto-animate": "^0.8.1", "@headlessui/vue": "^1.7.19", "@heroicons/vue": "^2.1.1", "apexcharts": "^3.47.0", @@ -420,6 +421,11 @@ "node": ">=12" } }, + "node_modules/@formkit/auto-animate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz", + "integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig==" + }, "node_modules/@headlessui/vue": { "version": "1.7.19", "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.19.tgz", diff --git a/package.json b/package.json index a061a3b1..f36d7d6b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "vue": "^3.3.13" }, "dependencies": { + "@formkit/auto-animate": "^0.8.1", "@headlessui/vue": "^1.7.19", "@heroicons/vue": "^2.1.1", "apexcharts": "^3.47.0", diff --git a/resources/js/Pages/Chat/ChatBaloon.vue b/resources/js/Pages/Chat/ChatBaloon.vue index f1889257..0be58fd3 100644 --- a/resources/js/Pages/Chat/ChatBaloon.vue +++ b/resources/js/Pages/Chat/ChatBaloon.vue @@ -12,14 +12,7 @@ const props = defineProps({ :class="message.from_ai ? 'flex-row-reverse' : 'flex-row'">
- - + :class="message.from_ai ? 'bg-gray-300/10 rounded-tr-none border-indigo-500' : 'bg-black/20 rounded-tl-none flex-row-reverse'">
-import {useForm, usePage} from "@inertiajs/vue3"; +import {router, useForm, usePage} from "@inertiajs/vue3"; import SampleQuestions from "./SampleQuestions.vue"; import { ChevronDoubleDownIcon, ChevronRightIcon} from "@heroicons/vue/20/solid"; import {computed, inject, onUnmounted, ref, watch} from "vue"; +import axios from "axios"; +import { useToast } from "vue-toastification"; + +const toast = useToast(); const props = defineProps({ loading: { @@ -12,8 +16,6 @@ const props = defineProps({ chat: Object, }) -const system_prompt = inject('system_prompt') - const emits = defineEmits(['chatSubmitted']) const errors = ref({}) @@ -24,9 +26,23 @@ const form = useForm({ const getting_results = ref(false) - const save = () => { getting_results.value = true + axios.post(route('chats.messages.create', { + chat: props.chat.id + }), { + input: form.input + }).then(response => { + getting_results.value = false + console.log(response.data.message) + router.reload({ + preserveScroll: true + }); + }).catch(error => { + getting_results.value = false + toast.error('An error occurred. Please try again.') + console.log(error) + }); } const setQuestion = (question) => { @@ -56,24 +72,29 @@ const setQuestion = (question) => { v-model="form.input" placeholder="Chat about your Collection"/> + v-if="getting_results" + class="mt-2"> + + + + + - + bg-gray-850 hover:text-gray-400 text-gray-500 px-2.5 rounded-r-md"> + + + + +
diff --git a/resources/js/Pages/Chat/ChatMessage.vue b/resources/js/Pages/Chat/ChatMessage.vue index f4eb4d5f..31f2f9c7 100644 --- a/resources/js/Pages/Chat/ChatMessage.vue +++ b/resources/js/Pages/Chat/ChatMessage.vue @@ -21,7 +21,7 @@ const toast = useToast();