diff --git a/composer.lock b/composer.lock index e16a0e4..96d3bb9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5db6b7e4a69c3e4bfc31d7d4f2bde9cd", + "content-hash": "455407a01daeb19b75bf4d7e31868f85", "packages": [ { "name": "aws/aws-crt-php", @@ -1727,56 +1727,6 @@ ], "time": "2023-12-03T19:50:20+00:00" }, - { - "name": "hollodotme/fast-cgi-client", - "version": "v3.1.7", - "source": { - "type": "git", - "url": "https://github.com/hollodotme/fast-cgi-client.git", - "reference": "062182d4eda73c161cc2839783acc83096ec0f37" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hollodotme/fast-cgi-client/zipball/062182d4eda73c161cc2839783acc83096ec0f37", - "reference": "062182d4eda73c161cc2839783acc83096ec0f37", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.1" - }, - "require-dev": { - "ext-xdebug": ">=2.6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "hollodotme\\FastCGI\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Holger Woltersdorf", - "email": "hw@hollo.me" - } - ], - "description": "A PHP fast CGI client to send requests (a)synchronously to PHP-FPM.", - "keywords": [ - "Socket", - "async", - "fastcgi", - "php-fpm" - ], - "support": { - "issues": "https://github.com/hollodotme/fast-cgi-client/issues", - "source": "https://github.com/hollodotme/fast-cgi-client/tree/v3.1.7" - }, - "time": "2021-12-07T10:10:20+00:00" - }, { "name": "jfcherng/php-color-output", "version": "3.0.0", @@ -2677,85 +2627,6 @@ }, "time": "2024-09-23T13:32:56+00:00" }, - { - "name": "laravel/vapor-core", - "version": "v2.37.1", - "source": { - "type": "git", - "url": "https://github.com/laravel/vapor-core.git", - "reference": "9fac8cd0f9b6979887253dd676e04ecb868be615" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/vapor-core/zipball/9fac8cd0f9b6979887253dd676e04ecb868be615", - "reference": "9fac8cd0f9b6979887253dd676e04ecb868be615", - "shasum": "" - }, - "require": { - "aws/aws-sdk-php": "^3.80", - "guzzlehttp/guzzle": "^6.3|^7.0", - "guzzlehttp/promises": "^1.4|^2.0", - "hollodotme/fast-cgi-client": "^3.0", - "illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/queue": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "monolog/monolog": "^1.12|^2.0|^3.2", - "nyholm/psr7": "^1.0", - "php": "^7.2|^8.0", - "riverline/multipart-parser": "^2.0.9", - "symfony/process": "^4.3|^5.0|^6.0|^7.0", - "symfony/psr-http-message-bridge": "^1.0|^2.0|^6.4|^7.0" - }, - "require-dev": { - "mockery/mockery": "^1.2", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.0|^9.0|^10.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - }, - "laravel": { - "providers": [ - "Laravel\\Vapor\\VaporServiceProvider" - ], - "aliases": { - "Vapor": "Laravel\\Vapor\\Vapor" - } - } - }, - "autoload": { - "files": [ - "src/debug.php" - ], - "psr-4": { - "Laravel\\Vapor\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The kernel and invocation handlers for Laravel Vapor", - "homepage": "https://github.com/laravel/vapor-core", - "keywords": [ - "laravel", - "vapor" - ], - "support": { - "source": "https://github.com/laravel/vapor-core/tree/v2.37.1" - }, - "time": "2024-03-26T16:55:06+00:00" - }, { "name": "league/commonmark", "version": "2.5.3", @@ -4306,84 +4177,6 @@ ], "time": "2024-10-15T16:15:16+00:00" }, - { - "name": "nyholm/psr7", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/Nyholm/psr7.git", - "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", - "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0", - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.9", - "php-http/message-factory": "^1.0", - "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", - "symfony/error-handler": "^4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Nyholm\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "A fast PHP7 implementation of PSR-7", - "homepage": "https://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.2" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2024-09-09T07:06:30+00:00" - }, { "name": "openai-php/client", "version": "v0.10.2", @@ -5936,62 +5729,6 @@ ], "time": "2024-04-27T21:32:50+00:00" }, - { - "name": "riverline/multipart-parser", - "version": "2.1.2", - "source": { - "type": "git", - "url": "https://github.com/Riverline/multipart-parser.git", - "reference": "7a9f4646db5181516c61b8e0225a343189beedcd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Riverline/multipart-parser/zipball/7a9f4646db5181516c61b8e0225a343189beedcd", - "reference": "7a9f4646db5181516c61b8e0225a343189beedcd", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=5.6.0" - }, - "require-dev": { - "laminas/laminas-diactoros": "^1.8.7 || ^2.11.1", - "phpunit/phpunit": "^5.7 || ^9.0", - "psr/http-message": "^1.0", - "symfony/psr-http-message-bridge": "^1.1 || ^2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Riverline\\MultiPartParser\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Romain Cambien", - "email": "romain@cambien.net" - }, - { - "name": "Riverline", - "homepage": "http://www.riverline.fr" - } - ], - "description": "One class library to parse multipart content with encoding and charset support.", - "keywords": [ - "http", - "multipart", - "parser" - ], - "support": { - "issues": "https://github.com/Riverline/multipart-parser/issues", - "source": "https://github.com/Riverline/multipart-parser/tree/2.1.2" - }, - "time": "2024-03-12T16:46:05+00:00" - }, { "name": "robsontenorio/mary", "version": "1.41.2", @@ -7948,89 +7685,6 @@ ], "time": "2024-09-25T14:20:29+00:00" }, - { - "name": "symfony/psr-http-message-bridge", - "version": "v7.1.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "f16471bb19f6685b9ccf0a2c03c213840ae68cd6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/f16471bb19f6685b9ccf0a2c03c213840ae68cd6", - "reference": "f16471bb19f6685b9ccf0a2c03c213840ae68cd6", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^6.4|^7.0" - }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-kernel": "<6.4" - }, - "require-dev": { - "nyholm/psr7": "^1.1", - "php-http/discovery": "^1.15", - "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0" - }, - "type": "symfony-bridge", - "autoload": { - "psr-4": { - "Symfony\\Bridge\\PsrHttpMessage\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "PSR HTTP message bridge", - "homepage": "https://symfony.com", - "keywords": [ - "http", - "http-message", - "psr-17", - "psr-7" - ], - "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.6" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, { "name": "symfony/routing", "version": "v7.1.6", diff --git a/resources/css/app.css b/resources/css/app.css index 0de2120..fb8bafd 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -5,3 +5,16 @@ [x-cloak] { display: none; } + +.ai-chat ul { + margin-left: 1.1rem; +} + +.ai-chat ul li { + padding-left: 1.1rem; + list-style-type: disc; +} + +.ai-chat li, .ai-chat p { + padding-bottom: .25em; +} \ No newline at end of file diff --git a/resources/views/holding/show.blade.php b/resources/views/holding/show.blade.php index e70fcfd..48067e1 100644 --- a/resources/views/holding/show.blade.php +++ b/resources/views/holding/show.blade.php @@ -208,7 +208,7 @@ Based on this current market data, quantity owned, and average cost basis, you should determine if the {$holding->symbol} holding is making or losing money. - Below is the question from the investor. Considering these facts, provide a concise response to the following question (give a direct response). Limit your response to no more than 75 words and consider using a common decision framework:" + Below is the question from the investor. Considering these facts, provide a concise response to the following question (give a direct response). Limit your response to no more than 75 words and consider using a common decision framework. Use github style markdown for any formatting:" ]) diff --git a/resources/views/livewire/ai-chat-window.blade.php b/resources/views/livewire/ai-chat-window.blade.php index e0846e3..8fc8d70 100644 --- a/resources/views/livewire/ai-chat-window.blade.php +++ b/resources/views/livewire/ai-chat-window.blade.php @@ -1,232 +1,209 @@ messages = $this->chatable->chats()->orderByRaw('created_at, id')->get(['role', 'content'])->toArray(); - } +use Mary\Traits\Toast; +use App\Models\AiChat; +use App\Models\Holding; +use Illuminate\Database\Eloquent\Model; +use Livewire\Volt\Component; +use OpenAI\Laravel\Facades\OpenAI; +use OpenAI\Responses\StreamResponse; - public function startCompletion($suggestedPrompt = null) - { - // prevent spam - if ($this->isRateLimited() || $this->streaming) { - array_push($this->messages, [ - 'role' => 'assistant', - 'content' => __('Hang on! You\'re doing that too much.') - ]); - $this->js('scrollChatWindow(250)'); - return; - } +new class extends Component { - if ($suggestedPrompt) { - $this->prompt = $suggestedPrompt; - } + use Toast; - if (empty(trim($this->prompt))) { - $this->resetPrompt(); + // props + public Model $chatable; + public string $system_prompt = 'You are an investment portfolio assistant providing advice to an investor. Use the following information to provide relevant recommendations. Use the words \'likely\' or \'may\' instead of concrete statements (except for obvious statements of fact or common sense). Use github style markdown for any formatting.'; + public array $suggested_prompts = []; + + public array $messages = []; + public ?string $prompt = null; + public ?string $answer = null; + public bool $streaming = false; - array_push($this->messages, ['role' => 'assistant', 'content' => __('Feel free to ask me a question!')]); - $this->js('scrollChatWindow(250)'); - - return; - } - - $this->chatable->chats()->save(new AiChat(['role' => 'user', 'content' => $this->prompt])); - array_push($this->messages, ['role' => 'user', 'content' => $this->prompt]); + // methods + public function mount() + { + $this->messages = $this->chatable->chats()->orderByRaw('created_at, id')->limit(25)->get(['role', 'content'])->toArray(); + } + + public function startCompletion($suggestedPrompt = null) + { + // prevent spam + if ($this->isRateLimited() || $this->streaming) { + array_push($this->messages, [ + 'role' => 'assistant', + 'content' => __('Hang on! You\'re doing that too much.') + ]); $this->js('scrollChatWindow(250)'); - - $this->resetPrompt(); - - $this->streaming = true; - $this->js('$wire.generate()'); + return; } + + if ($suggestedPrompt) { + $this->prompt = $suggestedPrompt; + } + + if (empty(trim($this->prompt))) { + $this->resetPrompt(); + + array_push($this->messages, ['role' => 'assistant', 'content' => __('Feel free to ask me a question!')]); + $this->js('scrollChatWindow(250)'); + + return; + } + + $this->chatable->chats()->save(new AiChat(['role' => 'user', 'content' => $this->prompt])); + array_push($this->messages, ['role' => 'user', 'content' => $this->prompt]); + $this->js('scrollChatWindow(250)'); + + $this->resetPrompt(); + + $this->streaming = true; + $this->js('$wire.generate()'); + } + + public function generate(): void + { - public function generate(): void - { + try { + $stream = OpenAI::chat()->createStreamed([ + 'model' => config('openai.model'), + 'messages' => [ + ['role' => 'system', 'content' => $this->system_prompt], + ...array_slice($this->messages, -10) + ], + ]); + } catch (\Exception $e) { + + $this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $e->getMessage()])); + array_push($this->messages, ['role' => 'assistant', 'content' => $e->getMessage()]); + $this->resetPrompt(); + return; + } + + $this->stream(to: "answer", content: '', replace: true); - try { - $stream = OpenAI::chat()->createStreamed([ - 'model' => config('openai.model'), - 'messages' => [ - ['role' => 'system', 'content' => $this->system_prompt], - ...array_slice($this->messages, -10) - ], - ]); - } catch (\Exception $e) { - - $this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $e->getMessage()])); - array_push($this->messages, ['role' => 'assistant', 'content' => $e->getMessage()]); - $this->resetPrompt(); - return; - } - - $this->stream(to: "answer", content: '', replace: true); + foreach($stream as $response){ - foreach($stream as $response){ - - if(!empty($response->choices[0]->delta->content)) { - $this->stream(to: 'answer', content: $response->choices[0]->delta->content, replace: false); - $this->answer .= $response->choices[0]->delta->content; - } - $this->js('scrollChatWindow()'); + if(!empty($response->choices[0]->delta->content)) { + $this->stream(to: 'answer', content: $response->choices[0]->delta->content, replace: false); + $this->answer .= $response->choices[0]->delta->content; } - - $this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $this->answer])); - array_push($this->messages, ['role' => 'assistant', 'content' => $this->answer]); - $this->resetPrompt(); + $this->js('scrollChatWindow()'); } - public function resetPrompt(): void - { - $this->answer = null; - $this->prompt = null; - $this->streaming = false; + $this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $this->answer])); + array_push($this->messages, ['role' => 'assistant', 'content' => $this->answer]); + $this->resetPrompt(); + } + + public function resetPrompt(): void + { + $this->answer = null; + $this->prompt = null; + $this->streaming = false; + } + + public function isRateLimited(): bool + { + $rateLimitKey = auth()->id() . '/' . $this->chatable->id; + + if (RateLimiter::tooManyAttempts($rateLimitKey, 20)) { + + return true; } - public function isRateLimited(): bool - { - $rateLimitKey = auth()->id() . '/' . $this->chatable->id; + RateLimiter::hit($rateLimitKey, 60); - if (RateLimiter::tooManyAttempts($rateLimitKey, 20)) { - - return true; - } + return false; + } - RateLimiter::hit($rateLimitKey, 60); +}; ?> - return false; - } - - }; ?> - -
+ + + + + + +
- - - - - -
+ + {{-- close button --}} + + + {{-- chat window --}} +
+ +
+ + + +

+ AI {{ __('Hi, how can I help?') }} + +

+
- - {{-- close button --}} - - - {{-- chat window --}} -
- -
- - - -

- AI {{ __('Hi, how can I help?') }} - -

-
- - @foreach($messages as $message) - - @if ($message['role'] == 'user') -
- - - - - -

- {{ __('You') }} {{ $message['content'] }} -

-
- - @else -
- - - -

- AI {{ $message['content'] }} -

-
- @endif - - @endforeach - - @if($streaming) -
+ @foreach($messages as $message) + + @if ($message['role'] == 'user') +
+ + + + + +

+ {{ __('You') }} {{ $message['content'] }} +

+
+ + @else +
-

- AI {{ $answer }} -

+
+ AI {!! Str::markdown($message['content']) !!} +
@endif + + @endforeach + + @if($streaming) +
+ + + +

+ AI {{ $answer }} +

+
+ @endif +
+ + {{-- prompt input --}} +
+
+ @foreach($suggested_prompts as $prompt) + + @endforeach +
- {{-- prompt input --}} - -
- @foreach($suggested_prompts as $prompt) - - @endforeach - -
- -
- -
- - -
- - -
+
-
-

{{ __('Advice generated by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }}

+
+ +
- - -
+ + +
+ +
+

{{ __('Advice generated by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }}

+
+ +
- \ No newline at end of file +
diff --git a/resources/views/portfolio/show.blade.php b/resources/views/portfolio/show.blade.php index 261871a..6d442eb 100644 --- a/resources/views/portfolio/show.blade.php +++ b/resources/views/portfolio/show.blade.php @@ -174,7 +174,7 @@ Based on the current market data, quantity owned, and average cost basis, you can determine the performance of any holding. - Below is the question from the investor. Considering these facts, provide a concise response to the following question (give a direct response). Limit your response to no more than 75 words and consider using a common decision framework:" + Below is the question from the investor. Considering these facts, provide a concise response to the following question (give a direct response). Limit your response to no more than 75 words and consider using a common decision framework. Use github style markdown for any formatting:" ])