Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions app/Actions/Github/Auth/HandleGithubInstallation.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@
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;

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);

return SourceCodeAccount::updateOrCreate([
'team_id' => $team->id,
'team_id' => $team?->id,
'provider' => SourceCodeProvider::GitHub,
'external_id' => data_get($installation, 'account.id'),
], [
Expand All @@ -30,16 +32,34 @@ public function handle(string $installationId, Team $team): SourceCodeAccount
'refresh_token' => null,
'installation_id' => $installationId,
'expires_at' => $expiresAt,
'impersonate_token' => $impersonateToken
]);
}

public function asController(Request $request): \Illuminate\Http\RedirectResponse
{
$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;
Comment on lines +52 to +63

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to use Laravel's HTTP client

}
}
18 changes: 10 additions & 8 deletions app/Actions/Github/HandleWebhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions app/Actions/Github/RegisterWebhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mola!

'active' => true,
]);

Expand Down

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you use the SourceCodeProvider of Github here? We have the repo and everything :-) Also, using it will make it easier for when you implement Gitlab and Bitbucket!

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Actions\PullRequestAssistant\Github;

use App\Services\GithubService;
use Lorisleiva\Actions\Concerns\AsAction;

class GetOriginalFileFromRepository
{
use AsAction;

public function __construct(
private GithubService $githubService
)
{}

public function handle(string $repositoryOwner, string $repository, string $branch, string $filePath)
{
return $this->githubService->getFileFromRepository($repositoryOwner, $repository, $branch, $filePath);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace App\Actions\PullRequestAssistant\Github;

use Exception;
use Illuminate\Http\Request;
use Lorisleiva\Actions\Concerns\AsAction;

class HandlePullRequestComment
{
use AsAction;

public const COMMIT_MESSAGE = 'Codex - PR Assitant commit';

public function handle(Request $request): bool
{
if($request->get('action') !== 'created') {
return false;
}

if(!str($request->input('comment.body'))->startsWith('/codex')) {
return false;
}

$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 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
{
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);
}
}
20 changes: 20 additions & 0 deletions app/Actions/PullRequestAssistant/Github/PushNewFileToGithub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Actions\PullRequestAssistant\Github;

use App\Services\GithubService;
use Lorisleiva\Actions\Concerns\AsAction;

class PushNewFileToGithub
{
use AsAction;

public function __construct(
private GithubService $githubService
){}

public function handle(string $repository, string $branch, string $filePath, string $owner, string $newContent, string $commitMessage)
{
$this->githubService->pushNewContentToRepository($repository, $branch, $filePath, $owner, $newContent, $commitMessage);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To refactor and add into the SourceCodeProvider

}
}
35 changes: 35 additions & 0 deletions app/Actions/PullRequestAssistant/Github/SendRequestToLLM.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Actions\PullRequestAssistant\Github;

use App\LLM\Contracts\Llm;
use GuzzleHttp\Client;
use Lorisleiva\Actions\Concerns\AsAction;

class SendRequestToLLM
{
use AsAction;

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;
$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 [];
}
Comment on lines +15 to +23

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a Prompt class like you made for the Technical description?


return json_decode($formattedResponse[0]);
}


private function formatResponse($content)
{
preg_match("/\[(.*?)\]/", $content, $formattedResponse);

return $formattedResponse;
}
}
42 changes: 42 additions & 0 deletions app/Clients/GithubClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Clients;

use App\Clients\Requests\Github\Contract\GithubRequest;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

class GithubClient {

private Client $client;

public function send(GithubRequest $request) {
$this->setClient($request);
$options = $this->getOptions($request);
$response = $this->client->request($request->getMethod(), $request->getUri(), $options);
return $request->transformResponse(json_decode($response->getBody()->getContents(), true));
}

private function setClient(GithubRequest $githubRequest)
{
$this->client = new Client([
'base_uri' => config('services.github.api_endpoint'),
'headers' => [
'Authorization' => 'Bearer '.$githubRequest->getAccessToken(),
'Accept' => 'application/vnd.github.v3+json',
]
]);
Comment on lines +22 to +28

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely clear why we should use raw HTTP calls when we have also the libraries of GH, GL & BB installed! Can you explain the reasoning? :-)

}

private function getOptions(GithubRequest $request): array
{
$options = [];

if($request->getBody()) {
$options['body'] = json_encode($request->getBody());
}

return $options;

}
}
21 changes: 21 additions & 0 deletions app/Clients/Requests/Github/Contract/GithubRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Clients\Requests\Github\Contract;

use App\Models\SourceCodeAccount;

abstract class GithubRequest
{
public abstract function getMethod(): string;
public abstract function getUri(): string;
public abstract function getAccessToken(): ?string;

public function transformResponse(array $response): array|string
{
return $response;
}
Comment on lines +13 to +16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like this pattern but would like it more if the transform returns something typed

public function getBody(): ?array
{
return null;
}
}
36 changes: 36 additions & 0 deletions app/Clients/Requests/Github/GetFileRequest.php

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be changed to use library?

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Clients\Requests\Github;

use App\Clients\Requests\Github\Contract\GithubRequest;
use App\Enums\RestRequest;
use App\SourceCode\Traits\GetGithubAccessToken;

class GetFileRequest extends GithubRequest
{
use GetGithubAccessToken;

public function __construct(
private string $repositoryOwner,
private string $repository,
private string $branch,
private string $filePath
){}

public function getMethod(): string
{
return RestRequest::GET->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;
}
}
Loading