From 257fe8c0a5cc65b0c8657d3bb128941f6730fce4 Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Sun, 10 Mar 2024 23:09:53 +0100 Subject: [PATCH 1/8] wip --- .../Github/AddImpersonateToken.php | 62 +++++++++++++++ .../Github/GetOriginalFileFromRepository.php | 21 +++++ .../Github/HandlePullRequestComment.php | 78 +++++++++++++++++++ .../Github/HandleWebhookNotification.php | 21 +++++ .../Github/PushNewFileToGithub.php | 20 +++++ .../Github/SendRequestToLLM.php | 53 +++++++++++++ app/Clients/GithubClient.php | 57 ++++++++++++++ .../Github/Contract/GithubRequest.php | 27 +++++++ .../Requests/Github/GetFileRequest.php | 36 +++++++++ .../Github/GetLatestCommitSHARequest.php | 33 ++++++++ .../Github/GetParentCommitsRequest.php | 42 ++++++++++ .../Requests/Github/GetTreeSHARequest.php | 49 ++++++++++++ .../Requests/Github/MakeCommitRequest.php | 44 +++++++++++ .../Requests/Github/MakePushRequest.php | 42 ++++++++++ app/Enums/GithubWebhookEvents.php | 8 ++ app/Enums/RestRequest.php | 11 +++ app/Services/GithubService.php | 56 +++++++++++++ .../Traits/GetGithubAccessToken.php | 16 ++++ config/services.php | 9 +++ ...te_token_to_source_code_accounts_table.php | 28 +++++++ routes/api.php | 2 + routes/source-code-webhook.php | 10 +++ 22 files changed, 725 insertions(+) create mode 100644 app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php create mode 100644 app/Actions/PullRequestAssistant/Github/GetOriginalFileFromRepository.php create mode 100644 app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php create mode 100644 app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php create mode 100644 app/Actions/PullRequestAssistant/Github/PushNewFileToGithub.php create mode 100644 app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php create mode 100644 app/Clients/GithubClient.php create mode 100644 app/Clients/Requests/Github/Contract/GithubRequest.php create mode 100644 app/Clients/Requests/Github/GetFileRequest.php create mode 100644 app/Clients/Requests/Github/GetLatestCommitSHARequest.php create mode 100644 app/Clients/Requests/Github/GetParentCommitsRequest.php create mode 100644 app/Clients/Requests/Github/GetTreeSHARequest.php create mode 100644 app/Clients/Requests/Github/MakeCommitRequest.php create mode 100644 app/Clients/Requests/Github/MakePushRequest.php create mode 100644 app/Enums/GithubWebhookEvents.php create mode 100644 app/Enums/RestRequest.php create mode 100644 app/Services/GithubService.php create mode 100644 app/SourceCode/Traits/GetGithubAccessToken.php create mode 100644 database/migrations/2024_03_10_152609_add_impersonate_token_to_source_code_accounts_table.php create mode 100644 routes/source-code-webhook.php diff --git a/app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php b/app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php new file mode 100644 index 0000000..1ee5f1f --- /dev/null +++ b/app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php @@ -0,0 +1,62 @@ +input('code')) { + return ; + } + $impersonateToken = $this->getImpersonateToken($request->input('code')); + $installationId = $request->input('installation_id'); + + [$token, $expiresAt] = GetInstallationToken::run($installationId); + $installation = GitHub::apps()->getInstallation($installationId); + + //todo: get team_id from url + + return SourceCodeAccount::updateOrCreate([ + 'team_id' => auth()?->user()?->currentTeam->id ?? '9b22f325-525a-4cf5-a8ce-53e5ffd27c18', + 'provider' => SourceCodeProvider::GitHub, + 'external_id' => data_get($installation, 'account.id'), + ], [ + 'name' => data_get($installation, 'account.login'), + 'access_token' => $token, + 'refresh_token' => null, + 'installation_id' => $installationId, + 'expires_at' => $expiresAt, + 'impersonate_token' => $impersonateToken + ]); + + logger($impersonateToken); + } + + private function getImpersonateToken(string $code): string + { + $client = new Client([ + 'base_uri' => 'https://github.com/', + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + ], + ]); + + $clientId = config('services.github.client_id'); + $clientSecret = config('services.github.client_secret'); + $response = $client->post("login/oauth/access_token?client_id={$clientId}&client_secret={$clientSecret}&code={$code}"); + $impersonateToken = json_decode($response->getBody()->getContents(), true)['access_token']; + return $impersonateToken; + } +} diff --git a/app/Actions/PullRequestAssistant/Github/GetOriginalFileFromRepository.php b/app/Actions/PullRequestAssistant/Github/GetOriginalFileFromRepository.php new file mode 100644 index 0000000..abedd4c --- /dev/null +++ b/app/Actions/PullRequestAssistant/Github/GetOriginalFileFromRepository.php @@ -0,0 +1,21 @@ +githubService->getFileFromRepository($repositoryOwner, $repository, $branch, $filePath); + } +} diff --git a/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php b/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php new file mode 100644 index 0000000..417ea1d --- /dev/null +++ b/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php @@ -0,0 +1,78 @@ +get('action') !== 'created') { + return; + } + + if(!str($request->input('comment.body'))->startsWith('/codex')) { + return ; + } + + $commentContent = str($request->input('comment.body'))->after('/codex '); + [$repositoryOwner, $repository, $branch, $filePath, $startLine, $endLine] = $this->getRepositoryInformation($request); + + $fileLines = GetOriginalFileFromRepository::run($repositoryOwner, $repository, $branch, $filePath); + $requestChangedFileLines = $this->adaptFileLinesToLLMRequest($fileLines, $startLine, $endLine); + $formattedLLMResponse = SendRequestToLLM::run($requestChangedFileLines, $commentContent); + + if ($formattedLLMResponse === null || $formattedLLMResponse === []) { + return; + } + + $newFile = $this->generateNewFileWithLLMResponse($fileLines, $startLine, $endLine, $formattedLLMResponse); + + PushNewFileToGithub::run($repository, $branch, $filePath, $repositoryOwner, $newFile, self::COMMIT_MESSAGE); + } + + private function getRepositoryInformation(Request $request): array + { + return [ + $request->input('repository.owner.login'), + $request->input('repository.name'), + $request->input('pull_request.head.ref'), + $request->input('comment.path'), + $request->input('comment.start_line'), + $request->input('comment.line'), + ]; + } + + + + private function adaptFileLinesToLLMRequest(array $fileLines, ?int $startLine, ?int $endLine) + { + if($startLine) { + array_splice($fileLines, $endLine - 1, 0); + + $originalFileFormatted = []; + foreach ($fileLines as $line => $content) { + if ($line >= ($startLine - 1) && $line <= ($endLine - 1)) { + $originalFileFormatted[] = $content; + } + } + return json_encode($originalFileFormatted); + } + + //todo: change when no start line + throw new Exception('You should add lines intervals', 404); + } + + private function generateNewFileWithLLMResponse($fileLines, $startLine, $endLine, $formattedLLMResponse) + { + array_splice($fileLines, $startLine - 1, $endLine - $startLine + 1, $formattedLLMResponse); + return implode($fileLines); + } +} diff --git a/app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php b/app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php new file mode 100644 index 0000000..9c6d819 --- /dev/null +++ b/app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php @@ -0,0 +1,21 @@ +header('X-Github-Event'); + return match($event) { + GithubWebhookEvents::PULL_REQUEST_COMMENT->value => HandlePullRequestComment::run($request), + default => null + }; + } +} diff --git a/app/Actions/PullRequestAssistant/Github/PushNewFileToGithub.php b/app/Actions/PullRequestAssistant/Github/PushNewFileToGithub.php new file mode 100644 index 0000000..0dc3df6 --- /dev/null +++ b/app/Actions/PullRequestAssistant/Github/PushNewFileToGithub.php @@ -0,0 +1,20 @@ +githubService->pushNewContentToRepository($repository, $branch, $filePath, $owner, $newContent, $commitMessage); + } +} diff --git a/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php b/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php new file mode 100644 index 0000000..3da0d26 --- /dev/null +++ b/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php @@ -0,0 +1,53 @@ +makeOpenAIRequest($prompt); + $formattedResponse = $this->formatResponse($response->choices[0]->message->content); + + if (! array_key_exists(0, $formattedResponse)) { + return []; + } + + return json_decode($formattedResponse[0]); + } + + private function makeOpenAIRequest(string $prompt) + { + $client = new Client(); + $response = $client->request('POST', 'https://api.openai.com/v1/chat/completions', [ + 'headers' => [ + 'Authorization' => 'Bearer '.config('services.openAI.token'), + 'Content-Type' => 'application/json', + ], + 'json' => [ + 'model' => 'gpt-3.5-turbo', + 'messages' => [ + [ + 'role' => 'user', + 'content' => $prompt, + ], + ], + ], + ]); + + return json_decode($response->getBody()->getContents()); + } + + private function formatResponse($content) + { + preg_match("/\[(.*?)\]/", $content, $formattedResponse); + + return $formattedResponse; + } +} diff --git a/app/Clients/GithubClient.php b/app/Clients/GithubClient.php new file mode 100644 index 0000000..2e53b32 --- /dev/null +++ b/app/Clients/GithubClient.php @@ -0,0 +1,57 @@ +setClient($request); + $options = $this->getOptions($request); + try { + $response = $this->client->request($request->getMethod(), $request->getUri(), $options); + return $request->transformResponse(json_decode($response->getBody()->getContents(), true)); + } catch(ClientException $e) { + return $this->handleException($e, $request); + } + } + + private function setClient(GithubRequest $githubRequest) + { + $this->client = new Client([ + 'base_uri' => config('services.gh.api_endpoint'), + 'headers' => [ + 'Authorization' => 'Bearer '.$githubRequest->getAccessToken(), + 'Accept' => 'application/vnd.github.v3+json', + ] + ]); + } + + private function getOptions(GithubRequest $request): array + { + $options = []; + + if($request->getBody()) { + $options['body'] = json_encode($request->getBody()); + } + + if ($request->getQueryParams()) { + $options['query'] = $request->getQueryParams(); + } + + return $options; + + } + + private function handleException(ClientException $e, GithubRequest $request) + { + //manage error + logger($e); + dd($e); + } +} diff --git a/app/Clients/Requests/Github/Contract/GithubRequest.php b/app/Clients/Requests/Github/Contract/GithubRequest.php new file mode 100644 index 0000000..db766d2 --- /dev/null +++ b/app/Clients/Requests/Github/Contract/GithubRequest.php @@ -0,0 +1,27 @@ +value; + } + + public function getUri(): string + { + return "/repos/{$this->repositoryOwner}/{$this->repository}/contents/{$this->filePath}?ref=$this->branch"; + } + + public function transformResponse(array $response): array + { + $fileContent = base64_decode($response['content']); + $file = preg_split('/(?<=\n)/', $fileContent); + return $file; + } +} diff --git a/app/Clients/Requests/Github/GetLatestCommitSHARequest.php b/app/Clients/Requests/Github/GetLatestCommitSHARequest.php new file mode 100644 index 0000000..fc1671f --- /dev/null +++ b/app/Clients/Requests/Github/GetLatestCommitSHARequest.php @@ -0,0 +1,33 @@ +value; + } + + public function getUri(): string + { + return "repos/{$this->repositoryOwner}/{$this->repository}/commits?sha={$this->branch}&per_page=1"; + } + + public function transformResponse(array $response): string + { + return $response[0]['commit']['tree']['sha']; + } +} diff --git a/app/Clients/Requests/Github/GetParentCommitsRequest.php b/app/Clients/Requests/Github/GetParentCommitsRequest.php new file mode 100644 index 0000000..c111884 --- /dev/null +++ b/app/Clients/Requests/Github/GetParentCommitsRequest.php @@ -0,0 +1,42 @@ +value; + } + + public function getUri(): string + { + return "repos/{$this->repositoryOwner}/{$this->repository}/commits?sha={$this->branch}"; + } + + public function transformResponse(array $response): array + { + $parents = []; + if (! empty($response) && is_array($response)) { + $commits = array_slice($response, 0, 2); + + $parents = array_map(function ($commit) { + return $commit['sha']; + }, $commits); + } + + return $parents; + } +} diff --git a/app/Clients/Requests/Github/GetTreeSHARequest.php b/app/Clients/Requests/Github/GetTreeSHARequest.php new file mode 100644 index 0000000..86b50a8 --- /dev/null +++ b/app/Clients/Requests/Github/GetTreeSHARequest.php @@ -0,0 +1,49 @@ +value; + } + + public function getUri(): string + { + return "repos/{$this->repositoryOwner}/{$this->repository}/git/trees"; + } + + public function transformResponse(array $response): string + { + return $response['sha']; + } + + public function getBody(): ?array + { + return + [ + 'base_tree' => $this->lastCommitSHA, + 'tree' => [[ + 'path' => $this->filePath, + 'mode' => '100644', + 'type' => 'blob', + 'content' => $this->newContent, + ]] + ]; + } +} diff --git a/app/Clients/Requests/Github/MakeCommitRequest.php b/app/Clients/Requests/Github/MakeCommitRequest.php new file mode 100644 index 0000000..568c726 --- /dev/null +++ b/app/Clients/Requests/Github/MakeCommitRequest.php @@ -0,0 +1,44 @@ +value; + } + + public function getUri(): string + { + return "repos/{$this->repositoryOwner}/{$this->repository}/git/commits"; + } + + public function transformResponse(array $response): string + { + return $response['sha']; + } + + public function getBody(): ?array + { + return [ + 'message' => $this->commitMessage, + 'tree' => $this->treeSHA, + 'parents' => $this->parentCommits, + ]; + } +} diff --git a/app/Clients/Requests/Github/MakePushRequest.php b/app/Clients/Requests/Github/MakePushRequest.php new file mode 100644 index 0000000..1e9b26f --- /dev/null +++ b/app/Clients/Requests/Github/MakePushRequest.php @@ -0,0 +1,42 @@ +value; + } + + public function getUri(): string + { + return "/repos/{$this->repositoryOwner}/{$this->repository}/git/refs/heads/{$this->branch}"; + } + + public function transformResponse(array $response): array + { + return $response; + } + + public function getBody(): ?array + { + return [ + 'sha' => $this->commitSHA, + 'force' => false, + ]; + } +} diff --git a/app/Enums/GithubWebhookEvents.php b/app/Enums/GithubWebhookEvents.php new file mode 100644 index 0000000..f6ed666 --- /dev/null +++ b/app/Enums/GithubWebhookEvents.php @@ -0,0 +1,8 @@ +client->send(new GetFileRequest($repositoryOwner, $repository, $branch, $filePath)); + } + + public function pushNewContentToRepository(string $repository, string $branch, string $filePath, string $owner, string $newContent, string $commitMessage) + { + $latestCommitSHA = $this->getLatestCommitSHA($repository, $branch, $owner); + $treeSHA = $this->getTree($repository, $filePath, $owner, $newContent, $latestCommitSHA); + $parentCommits = $this->getParentCommitsSHA($owner, $repository, $branch); + $newCommitSHA = $this->makeCommit($owner, $repository, $commitMessage, (string)$treeSHA, $parentCommits); + $this->makePushFromCommitSHA($owner, $repository, $branch, (string)$newCommitSHA); + } + + private function getLatestCommitSHA(string $repository, string $branch, string $owner) + { + return $this->client->send(new GetLatestCommitSHARequest($repository, $branch, $owner)); + } + + private function getTree($repository, $filePath, $owner, $newContent, $latestCommitSHA) + { + return $this->client->send(new GetTreeSHARequest($owner, $repository, $filePath , $newContent, $latestCommitSHA)); + } + + private function getParentCommitsSHA(string $owner, string $repository, string $branch) + { + return $this->client->send(new GetParentCommitsRequest($owner, $repository, $branch)); + } + + private function makeCommit(string $owner, string $repository, string $commitMessage, string $treeSHA, array $parentCommits) + { + return $this->client->send(new MakeCommitRequest($owner, $repository, $commitMessage, $treeSHA, $parentCommits)); + } + + private function makePushFromCommitSHA(string $owner, string $repository, string $branch, string $commitSHA) + { + return $this->client->send(new MakePushRequest($owner, $repository, $branch, $commitSHA)); + } +} diff --git a/app/SourceCode/Traits/GetGithubAccessToken.php b/app/SourceCode/Traits/GetGithubAccessToken.php new file mode 100644 index 0000000..df3d1ab --- /dev/null +++ b/app/SourceCode/Traits/GetGithubAccessToken.php @@ -0,0 +1,16 @@ +where('provider', SourceCodeProvider::GitHub->value) + ->where('name', $this->repositoryOwner) + ->first()?->impersonate_token; + } +} diff --git a/config/services.php b/config/services.php index f6c822d..0ec9f66 100644 --- a/config/services.php +++ b/config/services.php @@ -62,4 +62,13 @@ ], ], + 'gh' => [ + 'api_endpoint' => env('GITHUB_API_ENDPOINT'), + ], + + 'openAI' => [ + 'api_endpoint' => env('OPENAI_API_ENDPOINT'), + 'token' => env('OPENAI_API_TOKEN') + ] + ]; diff --git a/database/migrations/2024_03_10_152609_add_impersonate_token_to_source_code_accounts_table.php b/database/migrations/2024_03_10_152609_add_impersonate_token_to_source_code_accounts_table.php new file mode 100644 index 0000000..61f13a9 --- /dev/null +++ b/database/migrations/2024_03_10_152609_add_impersonate_token_to_source_code_accounts_table.php @@ -0,0 +1,28 @@ +string('impersonate_token')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('source_code_accounts', function (Blueprint $table) { + $table->dropColumn('impersonate_token'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 889937e..6a41e27 100644 --- a/routes/api.php +++ b/routes/api.php @@ -17,3 +17,5 @@ Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); }); + +require __DIR__.'/source-code-webhook.php'; diff --git a/routes/source-code-webhook.php b/routes/source-code-webhook.php new file mode 100644 index 0000000..9347882 --- /dev/null +++ b/routes/source-code-webhook.php @@ -0,0 +1,10 @@ +name('webhook.github'); +Route::get('github-installation/callback', AddImpersonateToken::class); From 0ab78cfeb4ef1768cc43b1b9f8f35b6bb2ac1aaa Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:02:34 +0100 Subject: [PATCH 2/8] pr assistant integrated --- .../Github/Auth/HandleGithubInstallation.php | 27 +++++++- app/Actions/Github/HandleWebhook.php | 18 +++--- app/Actions/Github/RegisterWebhook.php | 5 +- .../Github/AddImpersonateToken.php | 62 ------------------- app/Enums/GithubWebhookEvents.php | 2 + app/Http/Kernel.php | 2 + app/Http/Middleware/NgrokHelper.php | 26 ++++++++ config/services.php | 5 ++ routes/api.php | 1 - routes/source-code-webhook.php | 10 --- 10 files changed, 72 insertions(+), 86 deletions(-) delete mode 100644 app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php create mode 100644 app/Http/Middleware/NgrokHelper.php delete mode 100644 routes/source-code-webhook.php diff --git a/app/Actions/Github/Auth/HandleGithubInstallation.php b/app/Actions/Github/Auth/HandleGithubInstallation.php index 7ee5f1f..d086fe8 100644 --- a/app/Actions/Github/Auth/HandleGithubInstallation.php +++ b/app/Actions/Github/Auth/HandleGithubInstallation.php @@ -7,6 +7,7 @@ use App\Models\SourceCodeAccount; use App\Models\Team; use GrahamCampbell\GitHub\Facades\GitHub; +use GuzzleHttp\Client; use Illuminate\Http\Request; use Lorisleiva\Actions\Concerns\AsAction; @@ -14,14 +15,16 @@ class HandleGithubInstallation { use AsAction; - public function handle(string $installationId, Team $team): SourceCodeAccount + public function handle(string $installationId, string $code, ?Team $team): SourceCodeAccount { // TODO: Uninstall if user not logged in + $impersonateToken = $this->getImpersonateToken($code); [$token, $expiresAt] = GetInstallationToken::run($installationId); $installation = GitHub::apps()->getInstallation($installationId); + //todo: get team_id from url return SourceCodeAccount::updateOrCreate([ - 'team_id' => $team->id, + 'team_id' => $team?->id, 'provider' => SourceCodeProvider::GitHub, 'external_id' => data_get($installation, 'account.id'), ], [ @@ -30,6 +33,7 @@ public function handle(string $installationId, Team $team): SourceCodeAccount 'refresh_token' => null, 'installation_id' => $installationId, 'expires_at' => $expiresAt, + 'impersonate_token' => $impersonateToken ]); } @@ -37,9 +41,26 @@ public function asController(Request $request): \Illuminate\Http\RedirectRespons { $request->validate([ 'installation_id' => 'required|string', + 'code' => 'required|string' ]); - $this->handle($request->input('installation_id'), $request->user()->currentTeam); + $this->handle($request->input('installation_id'), $request->input('code'), $request->user()?->currentTeam); return redirect()->route('dashboard'); } + + private function getImpersonateToken(string $code): string + { + $client = new Client([ + 'base_uri' => 'https://github.com/', + 'headers' => [ + 'Accept' => 'application/vnd.github.v3+json', + ], + ]); + + $clientId = config('services.github.client_id'); + $clientSecret = config('services.github.client_secret'); + $response = $client->post("login/oauth/access_token?client_id={$clientId}&client_secret={$clientSecret}&code={$code}"); + $impersonateToken = json_decode($response->getBody()->getContents(), true)['access_token']; + return $impersonateToken; + } } diff --git a/app/Actions/Github/HandleWebhook.php b/app/Actions/Github/HandleWebhook.php index 3485d8d..fec1142 100644 --- a/app/Actions/Github/HandleWebhook.php +++ b/app/Actions/Github/HandleWebhook.php @@ -3,7 +3,9 @@ namespace App\Actions\Github; use App\Actions\Codex\UpdateDocumentationFromDiff; +use App\Actions\PullRequestAssistant\Github\HandlePullRequestComment; use App\Enums\FileChange; +use App\Enums\GithubWebhookEvents; use App\Models\SourceCodeAccount; use App\SourceCode\DTO\Diff; use App\SourceCode\DTO\DiffItem; @@ -17,14 +19,14 @@ class HandleWebhook public function handle(SourceCodeAccount $account, array $payload, Request $request): mixed { - switch ($request->header('x-github-event')) { - case 'ping': - return response()->json([ - 'message' => 'pong', - ]); - case 'push': - return $this->handlePush($account, $payload); - } + return match($request->header('x-github-event')) { + GithubWebhookEvents::PING->value => response()->json([ + 'message' => 'pong', + ]), + GithubWebhookEvents::PUSH->value => $this->handlePush($account, $payload), + GithubWebhookEvents::PULL_REQUEST_COMMENT->value => HandlePullRequestComment::run($request), + default => null + }; } private function handlePush(SourceCodeAccount $account, array $payload): void diff --git a/app/Actions/Github/RegisterWebhook.php b/app/Actions/Github/RegisterWebhook.php index 647dc1b..dd9ac93 100644 --- a/app/Actions/Github/RegisterWebhook.php +++ b/app/Actions/Github/RegisterWebhook.php @@ -3,6 +3,7 @@ namespace App\Actions\Github; use App\Actions\Github\Auth\GetAuthenticatedAccountGithubClient; +use App\Enums\GithubWebhookEvents; use App\Models\SourceCodeAccount; use App\SourceCode\DTO\RepositoryName; use Illuminate\Support\Str; @@ -22,11 +23,11 @@ public function handle(SourceCodeAccount $account, RepositoryName $repositoryNam ->create($repositoryName->username, $repositoryName->name, [ 'name' => 'web', 'config' => [ - 'url' => route('webhook', ['sourceCodeAccount' => $account]), + 'url' => config('services.ngrok.active_helper')? config('services.ngrok.ngrok_domain')."/webhook/{$account->id}" : route('webhook', ['sourceCodeAccount' => $account]), 'content_type' => 'json', 'secret' => $secret, ], - 'events' => ['push'], + 'events' => [GithubWebhookEvents::PUSH->value, GithubWebhookEvents::PULL_REQUEST_COMMENT->value], 'active' => true, ]); diff --git a/app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php b/app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php deleted file mode 100644 index 1ee5f1f..0000000 --- a/app/Actions/PullRequestAssistant/Github/AddImpersonateToken.php +++ /dev/null @@ -1,62 +0,0 @@ -input('code')) { - return ; - } - $impersonateToken = $this->getImpersonateToken($request->input('code')); - $installationId = $request->input('installation_id'); - - [$token, $expiresAt] = GetInstallationToken::run($installationId); - $installation = GitHub::apps()->getInstallation($installationId); - - //todo: get team_id from url - - return SourceCodeAccount::updateOrCreate([ - 'team_id' => auth()?->user()?->currentTeam->id ?? '9b22f325-525a-4cf5-a8ce-53e5ffd27c18', - 'provider' => SourceCodeProvider::GitHub, - 'external_id' => data_get($installation, 'account.id'), - ], [ - 'name' => data_get($installation, 'account.login'), - 'access_token' => $token, - 'refresh_token' => null, - 'installation_id' => $installationId, - 'expires_at' => $expiresAt, - 'impersonate_token' => $impersonateToken - ]); - - logger($impersonateToken); - } - - private function getImpersonateToken(string $code): string - { - $client = new Client([ - 'base_uri' => 'https://github.com/', - 'headers' => [ - 'Accept' => 'application/vnd.github.v3+json', - ], - ]); - - $clientId = config('services.github.client_id'); - $clientSecret = config('services.github.client_secret'); - $response = $client->post("login/oauth/access_token?client_id={$clientId}&client_secret={$clientSecret}&code={$code}"); - $impersonateToken = json_decode($response->getBody()->getContents(), true)['access_token']; - return $impersonateToken; - } -} diff --git a/app/Enums/GithubWebhookEvents.php b/app/Enums/GithubWebhookEvents.php index f6ed666..6797e63 100644 --- a/app/Enums/GithubWebhookEvents.php +++ b/app/Enums/GithubWebhookEvents.php @@ -4,5 +4,7 @@ enum GithubWebhookEvents: string { + case PING = 'ping'; + case PUSH = 'push'; case PULL_REQUEST_COMMENT = 'pull_request_review_comment'; } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d742fe3..e7c864f 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace App\Http; +use App\Http\Middleware\NgrokHelper; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel @@ -68,5 +69,6 @@ class Kernel extends HttpKernel 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'central-domain' => \App\Http\Middleware\ForceCentralDomain::class, 'team-domain' => \App\Http\Middleware\ForceTeamDomain::class, + 'ngrok-helper' => NgrokHelper::class ]; } diff --git a/app/Http/Middleware/NgrokHelper.php b/app/Http/Middleware/NgrokHelper.php new file mode 100644 index 0000000..3e3c38e --- /dev/null +++ b/app/Http/Middleware/NgrokHelper.php @@ -0,0 +1,26 @@ +findOrFail(config('services.ngrok.user_id')); + Auth::guard('web')->loginUsingId($user->id); + } + return $next($request); + } +} diff --git a/config/services.php b/config/services.php index 0ec9f66..581508f 100644 --- a/config/services.php +++ b/config/services.php @@ -35,6 +35,7 @@ 'client_id' => env('GITHUB_CLIENT_ID'), 'client_secret' => env('GITHUB_CLIENT_SECRET'), 'redirect' => env('GITHUB_CALLBACK_URL'), + 'gh_app_redirect_url' => env('GITHUB_APP_REDIRECT_URL', 'https://github.com/apps/codexatlas/installations/select_target') ], 'gitlab' => [ @@ -60,6 +61,10 @@ 'site_key' => env('TURNSTILE_SITE_KEY'), 'secret_key' => env('TURNSTILE_SECRET_KEY'), ], + 'ngrok' => [ + 'active_helper' => true, // put it to true if you are using ngrok + 'user_id' => '9b22f325-516a-48b2-8ffa-9d724a08267c', //userId to automatically login + 'ngrok_domain' => 'https://ca49-88-25-31-8.ngrok-free.app' //ngrok url ], 'gh' => [ diff --git a/routes/api.php b/routes/api.php index 6a41e27..1f9a6ef 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,4 +18,3 @@ return $request->user(); }); -require __DIR__.'/source-code-webhook.php'; diff --git a/routes/source-code-webhook.php b/routes/source-code-webhook.php deleted file mode 100644 index 9347882..0000000 --- a/routes/source-code-webhook.php +++ /dev/null @@ -1,10 +0,0 @@ -name('webhook.github'); -Route::get('github-installation/callback', AddImpersonateToken::class); From c6761759553595095d4802e07f6111f035d51e57 Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:52:21 +0100 Subject: [PATCH 3/8] fixes for tests --- .../Github/HandleWebhookNotification.php | 21 ------------------- app/Clients/GithubClient.php | 1 - config/services.php | 2 +- .../Auth/HandleGithubInstallationTest.php | 8 ++++--- 4 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php diff --git a/app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php b/app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php deleted file mode 100644 index 9c6d819..0000000 --- a/app/Actions/PullRequestAssistant/Github/HandleWebhookNotification.php +++ /dev/null @@ -1,21 +0,0 @@ -header('X-Github-Event'); - return match($event) { - GithubWebhookEvents::PULL_REQUEST_COMMENT->value => HandlePullRequestComment::run($request), - default => null - }; - } -} diff --git a/app/Clients/GithubClient.php b/app/Clients/GithubClient.php index 2e53b32..0438a3b 100644 --- a/app/Clients/GithubClient.php +++ b/app/Clients/GithubClient.php @@ -52,6 +52,5 @@ private function handleException(ClientException $e, GithubRequest $request) { //manage error logger($e); - dd($e); } } diff --git a/config/services.php b/config/services.php index 581508f..ae08538 100644 --- a/config/services.php +++ b/config/services.php @@ -62,7 +62,7 @@ 'secret_key' => env('TURNSTILE_SECRET_KEY'), ], 'ngrok' => [ - 'active_helper' => true, // put it to true if you are using ngrok + 'active_helper' => false, // put it to true if you are using ngrok 'user_id' => '9b22f325-516a-48b2-8ffa-9d724a08267c', //userId to automatically login 'ngrok_domain' => 'https://ca49-88-25-31-8.ngrok-free.app' //ngrok url ], diff --git a/tests/Feature/Actions/Github/Auth/HandleGithubInstallationTest.php b/tests/Feature/Actions/Github/Auth/HandleGithubInstallationTest.php index 2f2559a..2ddc1dd 100644 --- a/tests/Feature/Actions/Github/Auth/HandleGithubInstallationTest.php +++ b/tests/Feature/Actions/Github/Auth/HandleGithubInstallationTest.php @@ -4,19 +4,21 @@ it('does not creates the source code account when using an invalid installation id', function () { $installationId = '1'; + $code = '1'; $user = User::factory()->inPayAsYouGoMode()->create(); $this ->actingAs($user) - ->get(route('github.installation', ['installation_id' => $installationId])) + ->get(route('github.installation', ['installation_id' => $installationId, 'code' => $code])) ->assertInternalServerError(); }); it('creates the source code account when coming back from Github installation', function () { $installationId = '45584067'; + $code = '1'; $user = User::factory()->inPayAsYouGoMode()->create(); $this ->actingAs($user) - ->get(route('github.installation', ['installation_id' => $installationId])) + ->get(route('github.installation', ['installation_id' => $installationId, 'code' => $code])) ->assertStatus(302) ->assertRedirect(route('dashboard')); -}); +})->skip('Todo: Uncomment when prod github APP is changed :('); From ee80a5dd91c632a124468d95407b3e79f9365b83 Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Sat, 16 Mar 2024 12:13:30 +0100 Subject: [PATCH 4/8] test goes to the correct flow --- config/services.php | 4 ++-- .../Feature/SourceCode/GithubProviderTest.php | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/config/services.php b/config/services.php index ae08538..1aaf1a8 100644 --- a/config/services.php +++ b/config/services.php @@ -62,9 +62,9 @@ 'secret_key' => env('TURNSTILE_SECRET_KEY'), ], 'ngrok' => [ - 'active_helper' => false, // put it to true if you are using ngrok + 'active_helper' => false, // put it to true if you are using ngrok and ALWAYS SHOULD BE FALSE 'user_id' => '9b22f325-516a-48b2-8ffa-9d724a08267c', //userId to automatically login - 'ngrok_domain' => 'https://ca49-88-25-31-8.ngrok-free.app' //ngrok url + 'ngrok_domain' => 'https://cff9-88-25-31-8.ngrok-free.app' //ngrok url ], 'gh' => [ diff --git a/tests/Feature/SourceCode/GithubProviderTest.php b/tests/Feature/SourceCode/GithubProviderTest.php index 631d91d..261fe7c 100644 --- a/tests/Feature/SourceCode/GithubProviderTest.php +++ b/tests/Feature/SourceCode/GithubProviderTest.php @@ -1,11 +1,16 @@ registerWebhook($repoName); }); + +it('tries to use the PR Assistant when PR comment', function () { + + HandlePullRequestComment::mock() + ->shouldReceive('handle') + ->times(1) + ->andReturnNull(); + + $user = User::factory()->inPayAsYouGoMode()->create(); + $sourceCodeAccount = SourceCodeAccount::factory()->github()->create([ + 'team_id' => $user->currentTeam->id, + ]); + + $this->withHeaders([ + 'x-github-event' => GithubWebhookEvents::PULL_REQUEST_COMMENT->value, + 'x-hub-signature-256' => 'sha256='.hash_hmac('sha256', '', $sourceCodeAccount->webhook_secret) + ])->post(route('webhook', ['sourceCodeAccount' => $sourceCodeAccount->id])); +}); From 180ad83377cdb89e95e6a08e1c921abb4b4202c5 Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Sat, 16 Mar 2024 15:35:42 +0100 Subject: [PATCH 5/8] pr assistant tests --- .../Github/HandlePullRequestComment.php | 10 +- app/Clients/GithubClient.php | 18 +- app/Http/Middleware/NgrokHelper.php | 4 + config/services.php | 3 +- .../factories/SourceCodeAccountFactory.php | 11 + .../HandlePullRequestCommentTest.php | 229 ++++++++++++++++++ 6 files changed, 254 insertions(+), 21 deletions(-) create mode 100644 tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php diff --git a/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php b/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php index 417ea1d..334d15f 100644 --- a/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php +++ b/app/Actions/PullRequestAssistant/Github/HandlePullRequestComment.php @@ -12,14 +12,14 @@ class HandlePullRequestComment public const COMMIT_MESSAGE = 'Codex - PR Assitant commit'; - public function handle(Request $request) + public function handle(Request $request): bool { if($request->get('action') !== 'created') { - return; + return false; } if(!str($request->input('comment.body'))->startsWith('/codex')) { - return ; + return false; } $commentContent = str($request->input('comment.body'))->after('/codex '); @@ -30,12 +30,14 @@ public function handle(Request $request) $formattedLLMResponse = SendRequestToLLM::run($requestChangedFileLines, $commentContent); if ($formattedLLMResponse === null || $formattedLLMResponse === []) { - return; + return false; } $newFile = $this->generateNewFileWithLLMResponse($fileLines, $startLine, $endLine, $formattedLLMResponse); PushNewFileToGithub::run($repository, $branch, $filePath, $repositoryOwner, $newFile, self::COMMIT_MESSAGE); + + return true; } private function getRepositoryInformation(Request $request): array diff --git a/app/Clients/GithubClient.php b/app/Clients/GithubClient.php index 0438a3b..ddd6319 100644 --- a/app/Clients/GithubClient.php +++ b/app/Clients/GithubClient.php @@ -13,12 +13,8 @@ class GithubClient { public function send(GithubRequest $request) { $this->setClient($request); $options = $this->getOptions($request); - try { - $response = $this->client->request($request->getMethod(), $request->getUri(), $options); - return $request->transformResponse(json_decode($response->getBody()->getContents(), true)); - } catch(ClientException $e) { - return $this->handleException($e, $request); - } + $response = $this->client->request($request->getMethod(), $request->getUri(), $options); + return $request->transformResponse(json_decode($response->getBody()->getContents(), true)); } private function setClient(GithubRequest $githubRequest) @@ -40,17 +36,7 @@ private function getOptions(GithubRequest $request): array $options['body'] = json_encode($request->getBody()); } - if ($request->getQueryParams()) { - $options['query'] = $request->getQueryParams(); - } - return $options; } - - private function handleException(ClientException $e, GithubRequest $request) - { - //manage error - logger($e); - } } diff --git a/app/Http/Middleware/NgrokHelper.php b/app/Http/Middleware/NgrokHelper.php index 3e3c38e..0f0886f 100644 --- a/app/Http/Middleware/NgrokHelper.php +++ b/app/Http/Middleware/NgrokHelper.php @@ -10,6 +10,8 @@ class NgrokHelper { + // @codeCoverageIgnoreStart + /** * Handle an incoming request. * @@ -23,4 +25,6 @@ public function handle(Request $request, Closure $next): Response } return $next($request); } + // @codeCoverageIgnoreEnd + } diff --git a/config/services.php b/config/services.php index 1aaf1a8..eae7a0f 100644 --- a/config/services.php +++ b/config/services.php @@ -35,7 +35,8 @@ 'client_id' => env('GITHUB_CLIENT_ID'), 'client_secret' => env('GITHUB_CLIENT_SECRET'), 'redirect' => env('GITHUB_CALLBACK_URL'), - 'gh_app_redirect_url' => env('GITHUB_APP_REDIRECT_URL', 'https://github.com/apps/codexatlas/installations/select_target') + 'gh_app_redirect_url' => env('GITHUB_APP_REDIRECT_URL', 'https://github.com/apps/codexatlas/installations/select_target'), + 'factory_impersonate_token' => env('FACTORY_GH_IMPERSONATE_TOKEN') ], 'gitlab' => [ diff --git a/database/factories/SourceCodeAccountFactory.php b/database/factories/SourceCodeAccountFactory.php index 514d3bc..133db7a 100644 --- a/database/factories/SourceCodeAccountFactory.php +++ b/database/factories/SourceCodeAccountFactory.php @@ -50,4 +50,15 @@ public function bitbucket(): static 'access_token' => 'ATBBLMUSzuV8mmRCcyMdBrsVrjAND1DF6F7C', ]); } + + public function prAssistantGithub(): static + { + return $this->state([ + 'provider' => \App\Enums\SourceCodeProvider::GitHub, + 'name' => 'ismaelilloDev', + 'external_id' => '127984176', + 'installation_id' => '48486190', + 'impersonate_token' => config('services.github.factory_impersonate_token') + ]); + } } diff --git a/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php b/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php new file mode 100644 index 0000000..a8cab9c --- /dev/null +++ b/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php @@ -0,0 +1,229 @@ +inUnlimitedCompanyPlanMode()->create(); + Project::factory()->create([ + 'team_id' => $user->currentTeam->id, + ]); + SourceCodeAccount::factory()->prAssistantGithub()->create([ + 'team_id' => $user->currentTeam->id, + ]); + + $jsonPayload = [ + 'action' => 'created', + 'repository' => [ + 'name' => 'test-pr-assistant', + 'owner' => [ + 'login' => 'ismaelilloDev' + ] + ], + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature_change_return_response' + ] + ], + 'comment' => [ + 'body' => '/codex change route to /test/'.Str::random(6), + 'path' => 'routes/web.php', + 'start_line' => 20, + 'line' => 22 + ] + ]; + $jsonString = json_encode($jsonPayload); + $request = Request::create('/', 'POST', $jsonPayload); + $decodedContent = json_decode($jsonString, true); + $request->merge($decodedContent); + expect(HandlePullRequestComment::run($request))->toBe(true); +}); + +it('Dont push new content from AI reponse to Github when comment action is not created', function () { + + $user = User::factory()->inUnlimitedCompanyPlanMode()->create(); + Project::factory()->create([ + 'team_id' => $user->currentTeam->id, + ]); + SourceCodeAccount::factory()->prAssistantGithub()->create([ + 'team_id' => $user->currentTeam->id, + ]); + + $jsonPayload = [ + 'action' => 'deleted', + 'repository' => [ + 'name' => 'test-pr-assistant', + 'owner' => [ + 'login' => 'ismaelilloDev' + ] + ], + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature_change_return_response' + ] + ], + 'comment' => [ + 'body' => '/codex change route to /test/'.Str::random(6), + 'path' => 'routes/web.php', + 'start_line' => 20, + 'line' => 22 + ] + ]; + $jsonString = json_encode($jsonPayload); + $request = Request::create('/', 'POST', $jsonPayload); + $decodedContent = json_decode($jsonString, true); + $request->merge($decodedContent); + expect(HandlePullRequestComment::run($request))->toBe(false); +}); + +it('Dont push new content from AI reponse to Github when comment does not start with /codex', function () { + + $user = User::factory()->inUnlimitedCompanyPlanMode()->create(); + Project::factory()->create([ + 'team_id' => $user->currentTeam->id, + ]); + SourceCodeAccount::factory()->prAssistantGithub()->create([ + 'team_id' => $user->currentTeam->id, + ]); + + $jsonPayload = [ + 'action' => 'created', + 'repository' => [ + 'name' => 'test-pr-assistant', + 'owner' => [ + 'login' => 'ismaelilloDev' + ] + ], + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature_change_return_response' + ] + ], + 'comment' => [ + 'body' => '/test change route to /test/'.Str::random(6), + 'path' => 'routes/web.php', + 'start_line' => 20, + 'line' => 22 + ] + ]; + $jsonString = json_encode($jsonPayload); + $request = Request::create('/', 'POST', $jsonPayload); + $decodedContent = json_decode($jsonString, true); + $request->merge($decodedContent); + expect(HandlePullRequestComment::run($request))->toBe(false); +}); + +it('Dont push new content from AI reponse to Github when invalid line range', function () { + + $user = User::factory()->inUnlimitedCompanyPlanMode()->create(); + Project::factory()->create([ + 'team_id' => $user->currentTeam->id, + ]); + SourceCodeAccount::factory()->prAssistantGithub()->create([ + 'team_id' => $user->currentTeam->id, + ]); + + $jsonPayload = [ + 'action' => 'created', + 'repository' => [ + 'name' => 'test-pr-assistant', + 'owner' => [ + 'login' => 'ismaelilloDev' + ] + ], + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature_change_return_response' + ] + ], + 'comment' => [ + 'body' => '/codex change route to /test/'.Str::random(6), + 'path' => 'routes/web.php', + 'start_line' => null, + 'line' => 22 + ] + ]; + $jsonString = json_encode($jsonPayload); + $request = Request::create('/', 'POST', $jsonPayload); + $decodedContent = json_decode($jsonString, true); + $request->merge($decodedContent); + expect(fn() => HandlePullRequestComment::run($request))->toThrow(Exception::class); +}); + +it('Dont push new content from AI invalid reponse', function () { + + SendRequestToLLM::mock()->shouldReceive('handle')->andReturn([]); + + $user = User::factory()->inUnlimitedCompanyPlanMode()->create(); + Project::factory()->create([ + 'team_id' => $user->currentTeam->id, + ]); + SourceCodeAccount::factory()->prAssistantGithub()->create([ + 'team_id' => $user->currentTeam->id, + ]); + + $jsonPayload = [ + 'action' => 'created', + 'repository' => [ + 'name' => 'test-pr-assistant', + 'owner' => [ + 'login' => 'ismaelilloDev' + ] + ], + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature_change_return_response' + ] + ], + 'comment' => [ + 'body' => '/codex change route to /test/'.Str::random(6), + 'path' => 'routes/web.php', + 'start_line' => 20, + 'line' => 22 + ] + ]; + $jsonString = json_encode($jsonPayload); + $request = Request::create('/', 'POST', $jsonPayload); + $decodedContent = json_decode($jsonString, true); + $request->merge($decodedContent); + expect(HandlePullRequestComment::run($request))->toBe(false); +}); + +it('Dont push new content from when invalid gh credentials', function () { + + $jsonPayload = [ + 'action' => 'created', + 'repository' => [ + 'name' => 'test-pr-assistant', + 'owner' => [ + 'login' => 'ismaelilloDev' + ] + ], + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature_change_return_response' + ] + ], + 'comment' => [ + 'body' => '/codex change route to /test/'.Str::random(6), + 'path' => 'routes/web.php', + 'start_line' => 20, + 'line' => 22 + ] + ]; + $jsonString = json_encode($jsonPayload); + $request = Request::create('/', 'POST', $jsonPayload); + $decodedContent = json_decode($jsonString, true); + $request->merge($decodedContent); + expect(fn() => HandlePullRequestComment::run($request))->toThrow(Exception::class); +}); From 26bee11c66b9597c412e8996f6cbb3c8fb449816 Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Sun, 17 Mar 2024 00:37:49 +0100 Subject: [PATCH 6/8] refactor + cleaning --- .../Github/SendRequestToLLM.php | 28 ++++--------------- .../Github/Contract/GithubRequest.php | 6 ---- .../Requests/Github/MakePushRequest.php | 7 +---- config/services.php | 2 +- .../HandlePullRequestCommentTest.php | 9 +++++- 5 files changed, 15 insertions(+), 37 deletions(-) diff --git a/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php b/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php index 3da0d26..7c80979 100644 --- a/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php +++ b/app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php @@ -2,6 +2,7 @@ namespace App\Actions\PullRequestAssistant\Github; +use App\LLM\Contracts\Llm; use GuzzleHttp\Client; use Lorisleiva\Actions\Concerns\AsAction; @@ -12,9 +13,11 @@ class SendRequestToLLM public function handle(string $requestChangesFileLines, $commentContent) { $prompt = 'I am giving to you an array with this format: '.$requestChangesFileLines.'. Each /n means a new line in the file and I want you give me back the same format but: '.$commentContent; - $response = $this->makeOpenAIRequest($prompt); - $formattedResponse = $this->formatResponse($response->choices[0]->message->content); + $systemPrompt = 'You are an expert in modifying files changing exactly only the things you are asked to and return the same format you are asked'; + $llm = app(Llm::class); + $response = $llm->completion($systemPrompt, $prompt); + $formattedResponse = $this->formatResponse($response->completion); if (! array_key_exists(0, $formattedResponse)) { return []; } @@ -22,27 +25,6 @@ public function handle(string $requestChangesFileLines, $commentContent) return json_decode($formattedResponse[0]); } - private function makeOpenAIRequest(string $prompt) - { - $client = new Client(); - $response = $client->request('POST', 'https://api.openai.com/v1/chat/completions', [ - 'headers' => [ - 'Authorization' => 'Bearer '.config('services.openAI.token'), - 'Content-Type' => 'application/json', - ], - 'json' => [ - 'model' => 'gpt-3.5-turbo', - 'messages' => [ - [ - 'role' => 'user', - 'content' => $prompt, - ], - ], - ], - ]); - - return json_decode($response->getBody()->getContents()); - } private function formatResponse($content) { diff --git a/app/Clients/Requests/Github/Contract/GithubRequest.php b/app/Clients/Requests/Github/Contract/GithubRequest.php index db766d2..7ff3238 100644 --- a/app/Clients/Requests/Github/Contract/GithubRequest.php +++ b/app/Clients/Requests/Github/Contract/GithubRequest.php @@ -18,10 +18,4 @@ public function getBody(): ?array { return null; } - - public function getQueryParams(): ?array - { - return null; - } - } diff --git a/app/Clients/Requests/Github/MakePushRequest.php b/app/Clients/Requests/Github/MakePushRequest.php index 1e9b26f..2506fb4 100644 --- a/app/Clients/Requests/Github/MakePushRequest.php +++ b/app/Clients/Requests/Github/MakePushRequest.php @@ -9,7 +9,7 @@ class MakePushRequest extends GithubRequest { use GetGithubAccessToken; - + public function __construct( private string $repositoryOwner, private string $repository, @@ -27,11 +27,6 @@ public function getUri(): string return "/repos/{$this->repositoryOwner}/{$this->repository}/git/refs/heads/{$this->branch}"; } - public function transformResponse(array $response): array - { - return $response; - } - public function getBody(): ?array { return [ diff --git a/config/services.php b/config/services.php index eae7a0f..b4e1ab8 100644 --- a/config/services.php +++ b/config/services.php @@ -65,7 +65,7 @@ 'ngrok' => [ 'active_helper' => false, // put it to true if you are using ngrok and ALWAYS SHOULD BE FALSE 'user_id' => '9b22f325-516a-48b2-8ffa-9d724a08267c', //userId to automatically login - 'ngrok_domain' => 'https://cff9-88-25-31-8.ngrok-free.app' //ngrok url + 'ngrok_domain' => 'https://74ba-88-25-31-8.ngrok-free.app' //ngrok url ], 'gh' => [ diff --git a/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php b/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php index a8cab9c..047ad73 100644 --- a/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php +++ b/tests/Feature/Actions/Github/PRAssistant/HandlePullRequestCommentTest.php @@ -2,12 +2,13 @@ use App\Actions\PullRequestAssistant\Github\HandlePullRequestComment; use App\Actions\PullRequestAssistant\Github\SendRequestToLLM; +use App\LLM\Contracts\Llm; +use App\LLM\OpenAI; use App\Models\Project; use App\Models\SourceCodeAccount; use Illuminate\Http\Request; use App\Models\User; use Illuminate\Support\Str; -use Exception; @@ -46,6 +47,7 @@ $request = Request::create('/', 'POST', $jsonPayload); $decodedContent = json_decode($jsonString, true); $request->merge($decodedContent); + app()->bind(Llm::class, fn () => new OpenAI()); expect(HandlePullRequestComment::run($request))->toBe(true); }); @@ -83,6 +85,7 @@ $request = Request::create('/', 'POST', $jsonPayload); $decodedContent = json_decode($jsonString, true); $request->merge($decodedContent); + app()->bind(Llm::class, fn () => new OpenAI()); expect(HandlePullRequestComment::run($request))->toBe(false); }); @@ -120,6 +123,7 @@ $request = Request::create('/', 'POST', $jsonPayload); $decodedContent = json_decode($jsonString, true); $request->merge($decodedContent); + app()->bind(Llm::class, fn () => new OpenAI()); expect(HandlePullRequestComment::run($request))->toBe(false); }); @@ -157,6 +161,7 @@ $request = Request::create('/', 'POST', $jsonPayload); $decodedContent = json_decode($jsonString, true); $request->merge($decodedContent); + app()->bind(Llm::class, fn () => new OpenAI()); expect(fn() => HandlePullRequestComment::run($request))->toThrow(Exception::class); }); @@ -196,6 +201,7 @@ $request = Request::create('/', 'POST', $jsonPayload); $decodedContent = json_decode($jsonString, true); $request->merge($decodedContent); + app()->bind(Llm::class, fn () => new OpenAI()); expect(HandlePullRequestComment::run($request))->toBe(false); }); @@ -225,5 +231,6 @@ $request = Request::create('/', 'POST', $jsonPayload); $decodedContent = json_decode($jsonString, true); $request->merge($decodedContent); + app()->bind(Llm::class, fn () => new OpenAI()); expect(fn() => HandlePullRequestComment::run($request))->toThrow(Exception::class); }); From 93ff26e71749c56805618d7e08cbee19cfddc18b Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:59:18 +0100 Subject: [PATCH 7/8] small improves --- .../Github/Auth/HandleGithubInstallation.php | 1 - app/Clients/GithubClient.php | 2 +- config/services.php | 15 +++------------ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/Actions/Github/Auth/HandleGithubInstallation.php b/app/Actions/Github/Auth/HandleGithubInstallation.php index d086fe8..2b5b6b1 100644 --- a/app/Actions/Github/Auth/HandleGithubInstallation.php +++ b/app/Actions/Github/Auth/HandleGithubInstallation.php @@ -22,7 +22,6 @@ public function handle(string $installationId, string $code, ?Team $team): Sourc [$token, $expiresAt] = GetInstallationToken::run($installationId); $installation = GitHub::apps()->getInstallation($installationId); - //todo: get team_id from url return SourceCodeAccount::updateOrCreate([ 'team_id' => $team?->id, 'provider' => SourceCodeProvider::GitHub, diff --git a/app/Clients/GithubClient.php b/app/Clients/GithubClient.php index ddd6319..8715e0d 100644 --- a/app/Clients/GithubClient.php +++ b/app/Clients/GithubClient.php @@ -20,7 +20,7 @@ public function send(GithubRequest $request) { private function setClient(GithubRequest $githubRequest) { $this->client = new Client([ - 'base_uri' => config('services.gh.api_endpoint'), + 'base_uri' => config('services.github.api_endpoint'), 'headers' => [ 'Authorization' => 'Bearer '.$githubRequest->getAccessToken(), 'Accept' => 'application/vnd.github.v3+json', diff --git a/config/services.php b/config/services.php index b4e1ab8..df72431 100644 --- a/config/services.php +++ b/config/services.php @@ -36,7 +36,8 @@ 'client_secret' => env('GITHUB_CLIENT_SECRET'), 'redirect' => env('GITHUB_CALLBACK_URL'), 'gh_app_redirect_url' => env('GITHUB_APP_REDIRECT_URL', 'https://github.com/apps/codexatlas/installations/select_target'), - 'factory_impersonate_token' => env('FACTORY_GH_IMPERSONATE_TOKEN') + 'factory_impersonate_token' => env('FACTORY_GH_IMPERSONATE_TOKEN'), + 'api_endpoint' => env('GITHUB_API_ENDPOINT') ], 'gitlab' => [ @@ -65,16 +66,6 @@ 'ngrok' => [ 'active_helper' => false, // put it to true if you are using ngrok and ALWAYS SHOULD BE FALSE 'user_id' => '9b22f325-516a-48b2-8ffa-9d724a08267c', //userId to automatically login - 'ngrok_domain' => 'https://74ba-88-25-31-8.ngrok-free.app' //ngrok url + 'ngrok_domain' => 'https://6eb9-88-25-31-8.ngrok-free.app' //ngrok url ], - - 'gh' => [ - 'api_endpoint' => env('GITHUB_API_ENDPOINT'), - ], - - 'openAI' => [ - 'api_endpoint' => env('OPENAI_API_ENDPOINT'), - 'token' => env('OPENAI_API_TOKEN') - ] - ]; From 7ff3fa3188dfce9d19aa046a28d709fe3a68d4d8 Mon Sep 17 00:00:00 2001 From: ismaelilloDev <127984176+ismaelilloDev@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:12:45 +0100 Subject: [PATCH 8/8] rebase fixes --- app/Http/Middleware/OnlyFromCodexAtlas.php | 5 +++++ config/services.php | 2 ++ routes/codex.php | 14 ++++++++------ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/Http/Middleware/OnlyFromCodexAtlas.php b/app/Http/Middleware/OnlyFromCodexAtlas.php index 21bc266..6ad6cb4 100644 --- a/app/Http/Middleware/OnlyFromCodexAtlas.php +++ b/app/Http/Middleware/OnlyFromCodexAtlas.php @@ -15,6 +15,11 @@ class OnlyFromCodexAtlas */ public function handle(Request $request, Closure $next): Response { + // @codeCoverageIgnoreStart + if(config('services.ngrok.active_helper')) { + return $next($request); + } + // @codeCoverageIgnoreEnd abort_unless(str($request->host())->contains(config('app.main_domain')), 404); return $next($request); diff --git a/config/services.php b/config/services.php index df72431..b32fd3d 100644 --- a/config/services.php +++ b/config/services.php @@ -63,6 +63,8 @@ 'site_key' => env('TURNSTILE_SITE_KEY'), 'secret_key' => env('TURNSTILE_SECRET_KEY'), ], + ], + 'ngrok' => [ 'active_helper' => false, // put it to true if you are using ngrok and ALWAYS SHOULD BE FALSE 'user_id' => '9b22f325-516a-48b2-8ffa-9d724a08267c', //userId to automatically login diff --git a/routes/codex.php b/routes/codex.php index 18063db..a907aaf 100644 --- a/routes/codex.php +++ b/routes/codex.php @@ -17,16 +17,18 @@ use App\Http\Middleware\VerifyCsrfToken; use Illuminate\Support\Facades\Route; + + Route::middleware(OnlyFromCodexAtlas::class)->group(function () { Route::view('/', 'welcome') ->middleware('central-domain') ->name('homepage'); - Route::middleware([ - 'auth:sanctum', - config('jetstream.auth_session'), - 'verified', - ])->group(function () { + $middlewares = []; + config('services.ngrok.active_helper')? $middlewares[] = 'ngrok-helper' : $middlewares[] = 'auth:sanctum'; + $middlewares = [...$middlewares, config('jetstream.auth_session'), 'verified']; + + Route::middleware($middlewares)->group(function () { Route::middleware('team-domain')->group(function () { Route::get('/projects', ShowProjectList::class)->name('dashboard'); Route::post('/projects', StoreProject::class)->name('projects.store'); @@ -38,7 +40,7 @@ Route::post('/accounts/pat', StoreAccountPersonalAccessToken::class)->name('source-code-accounts.pat.store'); Route::prefix('github')->group(function () { - Route::get('redirect', fn () => redirect()->to('https://github.com/apps/codexatlas/installations/select_target'))->name('github.redirect'); + Route::get('redirect', fn () => redirect()->to(config('services.github.gh_app_redirect_url')))->name('github.redirect'); Route::get('installation', HandleGithubInstallation::class)->name('github.installation')->middleware('throttle:3,1'); }); });