diff --git a/app/Actions/Codex/Architecture/SystemComponents/ProcessSystemComponent.php b/app/Actions/Codex/Architecture/SystemComponents/ProcessSystemComponent.php index 0f5e9ddb..76569484 100644 --- a/app/Actions/Codex/Architecture/SystemComponents/ProcessSystemComponent.php +++ b/app/Actions/Codex/Architecture/SystemComponents/ProcessSystemComponent.php @@ -63,18 +63,19 @@ public function handle(Branch $branch, File $file, int $order): void ); } else { $completion = $llm->describeFile($project, $file, PromptRequestType::DOCUMENT_FILE); + $branch->systemComponents()->updateOrCreate([ + 'path' => $file->path, + ], [ + 'order' => $order, + 'sha' => $file->sha, + 'path' => $file->path, + 'markdown_docs' => $this->generateMarkdown(json_decode($completion->completion, true), $file->path), + 'file_contents' => $team->stores_code ? $file->contents() : null, + 'json_docs' => json_decode($completion->completion, true), + 'status' => SystemComponentStatus::Generated, + ]); } - $branch->systemComponents()->updateOrCreate([ - 'path' => $file->path, - ], [ - 'order' => $order, - 'sha' => $file->sha, - 'path' => $file->path, - 'markdown_docs' => $this->generateMarkdown(json_decode($completion->completion, true), $file->path), - 'file_contents' => $team->stores_code ? $file->contents() : null, - 'json_docs' => json_decode($completion->completion, true), - 'status' => SystemComponentStatus::Generated, - ]); + ProcessingLogEntry::write($branch, $file->path, class_basename($llm), $llm->modelName(), $completion); // @codeCoverageIgnoreStart @@ -96,7 +97,9 @@ public function handle(Branch $branch, File $file, int $order): void private function generateMarkdown(?array $completion, $path): ?string { if(!$completion) { + // @codeCoverageIgnoreStart return null; + // @codeCoverageIgnoreEnd } $completion = FormatterHelper::convertArrayKeysToLowerCase($completion); diff --git a/app/Actions/Helper/GetPromptTokens.php b/app/Actions/Helper/GetPromptTokens.php new file mode 100644 index 00000000..d0f44593 --- /dev/null +++ b/app/Actions/Helper/GetPromptTokens.php @@ -0,0 +1,34 @@ +getForModel($model); + $tokens = $encoder->encode($prompt); + return count($tokens); + } + + // @codeCoverageIgnoreStart + public function asCommand(Command $command): void + { + $model = $command->argument('model'); + $prompt = $command->argument('prompt'); + + $tokensCount = $this->handle($model, $prompt); + $command->info($tokensCount); + } + // @codeCoverageIgnoreEnd +} diff --git a/app/Actions/Helper/GetTotalPromptRequestTokens.php b/app/Actions/Helper/GetTotalPromptRequestTokens.php new file mode 100644 index 00000000..72833738 --- /dev/null +++ b/app/Actions/Helper/GetTotalPromptRequestTokens.php @@ -0,0 +1,17 @@ +getPromptRequest($promptRequestType); + $this->handleClassTraits($promptRequest, $project, $file); $system = $promptRequest->systemPrompt($project, $file); $user = $promptRequest->userPrompt($project, $file); return $this->completion($system, $user); } + + private function handleClassTraits(&$promptRequest, $project, $file) + { + $classTraits = class_uses($promptRequest); + + if(in_array(HasProject::class, $classTraits)) { + $promptRequest->setProject($project); + } + + if(in_array(HasFileDTO::class, $classTraits)) { + $promptRequest->setFile($file); + } + } } diff --git a/app/LLM/DumbLocalLlm.php b/app/LLM/DumbLocalLlm.php index 0851a81e..d4ef192f 100644 --- a/app/LLM/DumbLocalLlm.php +++ b/app/LLM/DumbLocalLlm.php @@ -2,24 +2,38 @@ namespace App\LLM; +use App\Actions\Helper\GetTotalPromptRequestTokens; +use App\LLM\Contracts\CanSupportPartialRequestInterface; use App\LLM\Contracts\Llm; use App\LLM\Contracts\PromptRequest; use App\LLM\DTO\CompletionResponse; use App\LLM\PromptRequests\DumpLlm\DumpLlmPromptRequest; use App\LLM\PromptRequests\DumpLlm\DumpLlmPromptTechStackRequest; use App\LLM\PromptRequests\PromptRequestType; +use App\Traits\CanSupportPartialRequest; -class DumbLocalLlm extends Llm +class DumbLocalLlm extends Llm implements CanSupportPartialRequestInterface { + use CanSupportPartialRequest; + + public ?PromptRequest $promptRequest = null; + public function getPromptRequest(PromptRequestType $promptRequestType): PromptRequest { - return match ($promptRequestType) { + $this->promptRequest = match ($promptRequestType) { PromptRequestType::TECH_STACK => new DumpLlmPromptTechStackRequest(), default => new DumpLlmPromptRequest() }; + + return $this->promptRequest; } public function completion(string $systemPrompt, string $userPrompt): CompletionResponse + { + return $this->handlePartialCompletionResponse($this->promptRequest, 'lorem ipsum'); + } + + public function makeRequest(string $prompt, string $systemPrompt, ?string $previousAnswer = null): ?CompletionResponse { return CompletionResponse::make( completion: $this->getCompletionResponse($systemPrompt), @@ -30,6 +44,17 @@ public function completion(string $systemPrompt, string $userPrompt): Completion ); } + public function getMaxAmountOfTokens(): ?int + { + return 1; + } + + + public function getTotalPromptTokens(string $userPrompt, string $systemPrompt): int + { + return GetTotalPromptRequestTokens::run($userPrompt, $systemPrompt, config('services.openai.completion_model')); + } + public function modelName(): string { return 'dumb'; diff --git a/app/LLM/OpenAI.php b/app/LLM/OpenAI.php index 5bbac824..b1268362 100644 --- a/app/LLM/OpenAI.php +++ b/app/LLM/OpenAI.php @@ -2,6 +2,8 @@ namespace App\LLM; +use App\Actions\Helper\GetTotalPromptRequestTokens; +use App\LLM\Contracts\CanSupportPartialRequestInterface; use App\LLM\Contracts\HasApiKey; use App\LLM\Contracts\Llm; use App\LLM\Contracts\PromptRequest; @@ -9,20 +11,27 @@ use App\LLM\PromptRequests\OpenAI\DocumentFilePromptRequest; use App\LLM\PromptRequests\OpenAI\GenerateTechStackPromptRequest; use App\LLM\PromptRequests\PromptRequestType; +use App\Traits\CanSupportPartialRequest; use OpenAI\Client; -class OpenAI extends Llm implements HasApiKey +class OpenAI extends Llm implements HasApiKey, CanSupportPartialRequestInterface { + use CanSupportPartialRequest; + private ?string $key = null; private ?string $model = null; + public ?PromptRequest $promptRequest = null; + public function getPromptRequest(PromptRequestType $promptRequestType): PromptRequest { - return match ($promptRequestType) { + $this->promptRequest = match ($promptRequestType) { PromptRequestType::DOCUMENT_FILE => new DocumentFilePromptRequest(), PromptRequestType::TECH_STACK => new GenerateTechStackPromptRequest(), }; + + return $this->promptRequest; } public function withModel(string $model): static @@ -37,10 +46,59 @@ public function modelName(): string return $this->model ?? config('services.openai.completion_model'); } + public function checkApiKey(string $key): bool + { + try { + $this->client($key)->models()->list(); + } catch (\Exception $e) { + return false; + } + + return true; + } + + public function usingApiKey(?string $key): static + { + $this->key = $key; + + return $this; + } + + private function client(?string $key = null): Client + { + return (new \OpenAI\Factory()) + ->withApiKey($key ?? $this->key ?? config('openai.api_key')) + ->make(); + } + public function completion(string $systemPrompt, string $userPrompt): CompletionResponse + { + if(app()->environment('testing')) { + return $this->makeRequest($userPrompt, $systemPrompt); + } + + // @codeCoverageIgnoreStart + $const = $this->getConstPrompt(); + + $completionResponse = $this->handlePartialCompletionResponse($this->promptRequest, $const); + + if($completionResponse) { + return $completionResponse; + } + + return $this->makeRequest($userPrompt, $systemPrompt); + + // @codeCoverageIgnoreEnd + } + + public function makeRequest(string $prompt, string $systemPrompt, ?string $previousAnswer = null): ?CompletionResponse { $start = intval(microtime(true) * 1000); - // wrapping on a retry function to avoid the limit per minute error + if($previousAnswer) { + // @codeCoverageIgnoreStart + $prompt .= 'An this was the previous answer: '.$previousAnswer; + // @codeCoverageIgnoreEnd + } $response = retry(3, fn () => $this->client()->chat()->create([ 'model' => $this->modelName(), 'response_format' => [ "type" => "json_object" ], @@ -51,12 +109,13 @@ public function completion(string $systemPrompt, string $userPrompt): Completion ], [ 'role' => 'user', - 'content' => $userPrompt, + 'content' => $prompt, ], ], 'stop' => ['-----', "\nEND"], ]), 61500); $end = intval(microtime(true) * 1000); + $previousAnswer = $response->choices[0]->message->content; return CompletionResponse::make( completion: $response->choices[0]->message->content, processingTimeMilliseconds: $end - $start, @@ -66,28 +125,25 @@ public function completion(string $systemPrompt, string $userPrompt): Completion ); } - public function checkApiKey(string $key): bool + // @codeCoverageIgnoreStart + public function getTotalPromptTokens(string $userPrompt, string $systemPrompt): int { - try { - $this->client($key)->models()->list(); - } catch (\Exception $e) { - return false; - } - - return true; + return GetTotalPromptRequestTokens::run($userPrompt, $systemPrompt, $this->modelName()); } - public function usingApiKey(?string $key): static + public function getMaxAmountOfTokens(): ?int { - $this->key = $key; - - return $this; + $tokens = config('services.openai.tokens'); + return isset($tokens[$this->modelName()])? $tokens[$this->modelName()] : null; } - private function client(?string $key = null): Client + private function getConstPrompt(): ?string { - return (new \OpenAI\Factory()) - ->withApiKey($key ?? $this->key ?? config('openai.api_key')) - ->make(); + return match(true) { + $this->promptRequest instanceof GenerateTechStackPromptRequest => 'You are writing documentation for a file in a project with this name: '. $this->promptRequest->project?->name. '. These are the file dependencies of the project: ', + $this->promptRequest instanceof DocumentFilePromptRequest => 'You are writing documentation for a file in a project with this name: '. $this->promptRequest->project?->name. '. These is the content of the file: ' + }; } + // @codeCoverageIgnoreEnd + } diff --git a/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptRequest.php b/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptRequest.php index 86601751..96ab8e2a 100644 --- a/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptRequest.php +++ b/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptRequest.php @@ -5,9 +5,13 @@ use App\LLM\Contracts\PromptRequest; use App\Models\Project; use App\SourceCode\DTO\File; +use App\Traits\HasFileDTO; +use App\Traits\HasProject; class DumpLlmPromptRequest implements PromptRequest { + use HasProject, HasFileDTO; + public function systemPrompt(Project $project, File $file): string { return 'Lorem ipsum dolor sit amet'; diff --git a/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptTechStackRequest.php b/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptTechStackRequest.php index bed1f9af..38288875 100644 --- a/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptTechStackRequest.php +++ b/app/LLM/PromptRequests/DumpLlm/DumpLlmPromptTechStackRequest.php @@ -6,9 +6,13 @@ use App\LLM\PromptRequests\PromptRequestType; use App\Models\Project; use App\SourceCode\DTO\File; +use App\Traits\HasFileDTO; +use App\Traits\HasProject; class DumpLlmPromptTechStackRequest implements PromptRequest { + use HasFileDTO, HasProject; + public function systemPrompt(Project $project, File $file): string { return PromptRequestType::TECH_STACK->value; diff --git a/app/LLM/PromptRequests/OpenAI/DocumentFilePromptRequest.php b/app/LLM/PromptRequests/OpenAI/DocumentFilePromptRequest.php index f4c13505..568cc16e 100644 --- a/app/LLM/PromptRequests/OpenAI/DocumentFilePromptRequest.php +++ b/app/LLM/PromptRequests/OpenAI/DocumentFilePromptRequest.php @@ -5,12 +5,18 @@ use App\LLM\Contracts\PromptRequest; use App\Models\Project; use App\SourceCode\DTO\File; +use App\Traits\HasFileDTO; +use App\Traits\HasProject; class DocumentFilePromptRequest implements PromptRequest { + use HasProject, HasFileDTO; + public function systemPrompt(Project $project, File $file): string { - return 'You are an expert in writing software documentation. Write a short description of the provided in JSON format with the following structure: + return 'You are an expert in writing software documentation. + Sometimes you may receive previous json generated. In that case, do the merge of the new json with the old one. + Write a short description of the provided in JSON format with the following structure: [KEY] tldr [VALUE] General overview of what the file does diff --git a/app/LLM/PromptRequests/OpenAI/GenerateTechStackPromptRequest.php b/app/LLM/PromptRequests/OpenAI/GenerateTechStackPromptRequest.php index f6970682..d0881ef3 100644 --- a/app/LLM/PromptRequests/OpenAI/GenerateTechStackPromptRequest.php +++ b/app/LLM/PromptRequests/OpenAI/GenerateTechStackPromptRequest.php @@ -5,24 +5,33 @@ use App\LLM\Contracts\PromptRequest; use App\Models\Project; use App\SourceCode\DTO\File; +use App\Traits\HasFileDTO; +use App\Traits\HasProject; class GenerateTechStackPromptRequest implements PromptRequest { + use HasProject, HasFileDTO; + + public const INTRODUCE_PROJET_NAME = 'You are writing documentation for a file in a project with this name: '; + public const INTRODUCE_PROJECT_DEPENDENCIES = '. These are the file dependencies of the project: '; + public function systemPrompt(Project $project, File $file): string { - return 'You are an expert in writing tech stack files been able to determine the main frameworks/libraries used. Give me a JSON with the main frameworks/libraries that are used. + return 'You are an expert in writing tech stack files been able to determine the main frameworks used. Give me a JSON with the main frameworks that are used. The JSON should have a key with the name of the framework and the value should be medium description. - This is an example of the json {"Framework/Library name" : "Medium description of the Framework/Library" } + This is an example of the json {"Framework name" : "Medium description of the Framework" } + Sometimes you may receive previous json generated. In that case, do the merge of the new json with the old one. + The keys of the json shoul be a human format. Try to avoid things like laravel/framework and put the key as Laravel insetad. Some rules: - - Only talk about the most important frameworks/libraries. + - Only talk about the most important frameworks. - Do not output the original file.'; } public function userPrompt(Project $project, File $file): string { - return 'You are writing documentation for a file in the '.$project->name.' project. These are the file dependencies of the project: + return self::INTRODUCE_PROJET_NAME.$project->name.self::INTRODUCE_PROJECT_DEPENDENCIES.' ``` '.$file->contents().' ```'; diff --git a/app/Traits/CanSupportPartialRequest.php b/app/Traits/CanSupportPartialRequest.php new file mode 100644 index 00000000..123a2ddd --- /dev/null +++ b/app/Traits/CanSupportPartialRequest.php @@ -0,0 +1,91 @@ +systemPrompt($promptRequest->project(), $promptRequest->file()); + $userPrompt = $promptRequest->userPrompt($promptRequest->project(), $promptRequest->file()); + + $totalTokens = $this->getTotalPromptTokens($userPrompt, $systemPrompt); + $maxAmountOfTokens = $this->getMaxAmountOfTokens(); + + if($totalTokens < $maxAmountOfTokens || $maxAmountOfTokens === null) { + // @codeCoverageIgnoreStart + return null; + // @codeCoverageIgnoreEnd + } + + $subPromptsCount = ceil($totalTokens / $maxAmountOfTokens); + $subPrompts = $this->generateSubPrompts($subPromptsCount, $partialPromptConst); + return $this->handlePromptRequest($subPrompts, $systemPrompt); + } + + + private function generateSubPrompts(int $subPromptsCount, string $subPromptConst): array + { + $subPromts = []; + + $lines = explode("\n", $this->promptRequest->file->contents()); + $partSize = ceil(count($lines) / $subPromptsCount); + $dividedLines = array_chunk($lines, $partSize); + foreach($dividedLines as $line) { + $subPromts[] = $subPromptConst."\n".implode("\n", $line); + } + return $subPromts; + } + + + private function handlePromptRequest(array $subPromts, string $systemPrompt): ?CompletionResponse + { + $responses = []; + $previousAnswer = null; + + foreach($subPromts as $prompt) { + $completionResponse = $this->makeRequest($prompt, $systemPrompt, $previousAnswer); + $responses[] = $completionResponse; + $previousAnswer = $completionResponse->completion; + } + $completionResponse = $this->mergeCompletionResponses($responses); + return $completionResponse; + } + + private function mergeCompletionResponses(array $responses) { + + $completion = []; + $processingTimeMilliseconds = 0; + $inputTokens = 0; + $outputTokens = 0; + $totalTokens = 0; + + + foreach ($responses as $response) { + $jsonData = json_decode($response->completion, true); + $completion = $this->mergeJsons($jsonData, $completion); + $processingTimeMilliseconds += $response->processingTimeMilliseconds; + $inputTokens += $response->inputTokens; + $outputTokens += $response->outputTokens; + $totalTokens += $response->totalTokens; + } + + return new CompletionResponse(json_encode($completion), $processingTimeMilliseconds, $inputTokens, $outputTokens, $totalTokens); + } + + private function mergeJsons(array $newJson, array $completion) + { + + foreach ($newJson as $key => $value) { + if (is_array($value) && array_key_exists($key, $completion) && is_array($completion[$key])) { + $completion[$key] = $this->mergeJsons($value, $completion[$key]); + } else { + $completion[$key] = $value; + } + } + return $completion; + } +} diff --git a/app/Traits/HasFileDTO.php b/app/Traits/HasFileDTO.php new file mode 100644 index 00000000..4f1dfcfc --- /dev/null +++ b/app/Traits/HasFileDTO.php @@ -0,0 +1,20 @@ +file = $file; + } + + public function file(): File + { + return $this->file; + } +} diff --git a/app/Traits/HasProject.php b/app/Traits/HasProject.php new file mode 100644 index 00000000..6261b00d --- /dev/null +++ b/app/Traits/HasProject.php @@ -0,0 +1,20 @@ +project = $project; + } + + public function project(): Project + { + return $this->project; + } +} diff --git a/composer.json b/composer.json index 43cdd4f7..abb044ed 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "league/flysystem-aws-s3-v3": "^3.0", "livewire/livewire": "^3.0", "lorisleiva/laravel-actions": "^2.7", - "openai-php/laravel": "^0.7.8" + "openai-php/laravel": "^0.7.8", + "yethee/tiktoken": "^0.3.0" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.9", diff --git a/composer.lock b/composer.lock index 0c4b9db3..e7b48285 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": "9b97b45eb340d06050960689400a94a8", + "content-hash": "a2898ba94f87e4b0d0325c1653ea5aea", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -11822,6 +11822,55 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "yethee/tiktoken", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/yethee/tiktoken-php.git", + "reference": "c84f066dcb0cff60685537c71bf795967775446a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yethee/tiktoken-php/zipball/c84f066dcb0cff60685537c71bf795967775446a", + "reference": "c84f066dcb0cff60685537c71bf795967775446a", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/service-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "phpunit/phpunit": "^10.3", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "5.19.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Yethee\\Tiktoken\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP version of tiktoken", + "keywords": [ + "bpe", + "decode", + "encode", + "openai", + "tiktoken", + "tokenizer" + ], + "support": { + "issues": "https://github.com/yethee/tiktoken-php/issues", + "source": "https://github.com/yethee/tiktoken-php/tree/0.3.0" + }, + "time": "2024-01-10T10:34:57+00:00" } ], "packages-dev": [ diff --git a/config/services.php b/config/services.php index 157af0f5..6be0b40d 100644 --- a/config/services.php +++ b/config/services.php @@ -53,6 +53,10 @@ 'key' => env('OPENAI_API_KEY'), 'completion_model' => env('OPENAI_COMPLETION_MODEL', 'gpt-3.5-turbo-0125'), 'embeddings_model' => env('OPENAI_EMBEDDINGS_MODEL', 'text-embedding-ada-002'), + 'tokens' => [ + 'gpt-3.5-turbo-0125' => 16385, + 'gpt-4-turbo-preview' => 128000 + ] ], 'cloudflare' => [