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
157 changes: 157 additions & 0 deletions api/pkg/agent/skill/github/pr_create_review_comment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package github

import (
"context"
"fmt"
"strconv"

"github.com/google/go-github/v57/github"
"github.com/sashabaranov/go-openai"

"github.com/helixml/helix/api/pkg/agent"
"github.com/helixml/helix/api/pkg/types"
"github.com/helixml/helix/api/pkg/util/jsonschema"
)

const createReviewCommentSkillDescription = `Create a review comment on a GitHub pull request. Use it when you need to add a comment to a pull request.
If you provide file_path and line_number, the comment will be an inline review comment on that specific line.
If you omit file_path and line_number, the comment will be a general comment on the pull request.
DO NOT try to pass owner or repository name to this skill, they are set automatically to the correct values by the trigger.`

var createReviewCommentParameters = jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"content": {
Type: jsonschema.String,
Description: "The content of the comment",
},
"file_path": {
Type: jsonschema.String,
Description: "The relative file path to comment on (e.g. src/main.go). Required for inline comments.",
},
"line_number": {
Type: jsonschema.Integer,
Description: "The line number in the file to comment on. Starts at 1. Required for inline comments.",
},
},
Required: []string{"content"},
}

func NewCreateReviewCommentSkill(token, baseURL string) agent.Skill {
return agent.Skill{
Name: "GitHubCreateReviewComment",
Description: createReviewCommentSkillDescription,
Parameters: createReviewCommentParameters,
Direct: true,
Tools: []agent.Tool{
&GitHubCreateReviewCommentTool{
token: token,
baseURL: baseURL,
},
},
}
}

type GitHubCreateReviewCommentTool struct {
token string
baseURL string
}

func (t *GitHubCreateReviewCommentTool) Name() string {
return "GitHubCreateReviewComment"
}

func (t *GitHubCreateReviewCommentTool) Description() string {
return "Create a review comment on a GitHub pull request"
}

func (t *GitHubCreateReviewCommentTool) Icon() string {
return ""
}

func (t *GitHubCreateReviewCommentTool) String() string {
return "GitHubCreateReviewComment"
}

func (t *GitHubCreateReviewCommentTool) StatusMessage() string {
return "Creating a review comment on a GitHub pull request"
}

func (t *GitHubCreateReviewCommentTool) OpenAI() []openai.Tool {
return []openai.Tool{
{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: "GitHubCreateReviewComment",
Description: createReviewCommentSkillDescription,
Parameters: createReviewCommentParameters,
},
},
}
}

func (t *GitHubCreateReviewCommentTool) Execute(ctx context.Context, _ agent.Meta, args map[string]interface{}) (string, error) {
content, ok := args["content"].(string)
if !ok {
return "", fmt.Errorf("content is required")
}

ghCtx, ok := types.GetGitHubRepositoryContext(ctx)
if !ok {
return "", fmt.Errorf("github repository context not found")
}

client := NewClientWithPATAndBaseURL(t.token, t.baseURL)

content = fmt.Sprintf("[Helix] %s", content)

filePath, hasFilePath := args["file_path"].(string)
lineNumberRaw, hasLineNumber := args["line_number"]

if hasFilePath && hasLineNumber {
lineNumber, err := parseInt(lineNumberRaw)
if err != nil {
return "", fmt.Errorf("failed to parse line_number: %w", err)
}
if lineNumber < 1 {
lineNumber = 1
}

comment := &github.PullRequestComment{
Body: &content,
CommitID: &ghCtx.HeadSHA,
Path: &filePath,
Line: &lineNumber,
}

created, _, err := client.client.PullRequests.CreateComment(ctx, ghCtx.Owner, ghCtx.RepositoryName, ghCtx.PullRequestID, comment)
if err != nil {
return "", fmt.Errorf("failed to create inline review comment: %w", err)
}

return fmt.Sprintf("Inline review comment created on %s line %d (ID: %d)", filePath, lineNumber, created.GetID()), nil
}

comment := &github.IssueComment{
Body: &content,
}

created, _, err := client.client.Issues.CreateComment(ctx, ghCtx.Owner, ghCtx.RepositoryName, ghCtx.PullRequestID, comment)
if err != nil {
return "", fmt.Errorf("failed to create PR comment: %w", err)
}

return fmt.Sprintf("PR comment created (ID: %d)", created.GetID()), nil
}

func parseInt(value any) (int, error) {
switch v := value.(type) {
case int:
return v, nil
case float64:
return int(v), nil
case string:
return strconv.Atoi(v)
}
return 0, fmt.Errorf("invalid integer value")
}
116 changes: 116 additions & 0 deletions api/pkg/agent/skill/github/pull_request_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package github

import (
"context"
"fmt"
"strings"

"github.com/google/go-github/v57/github"
"github.com/sashabaranov/go-openai"

"github.com/helixml/helix/api/pkg/agent"
"github.com/helixml/helix/api/pkg/types"
"github.com/helixml/helix/api/pkg/util/jsonschema"
)

const pullRequestDiffSkillDescription = `Get the diff of a GitHub pull request, use it when you need to see the changes in a pull request.
DO NOT try to pass owner or repository name to this skill, they are set automatically to the correct values by the trigger.`

var pullRequestDiffParameters = jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{},
Required: []string{},
}

func NewPullRequestDiffSkill(token, baseURL string) agent.Skill {
return agent.Skill{
Name: "GitHubPullRequestDiff",
Description: pullRequestDiffSkillDescription,
Parameters: pullRequestDiffParameters,
Direct: true,
Tools: []agent.Tool{
&GitHubPullRequestDiffTool{
token: token,
baseURL: baseURL,
},
},
}
}

type GitHubPullRequestDiffTool struct {
token string
baseURL string
}

func (t *GitHubPullRequestDiffTool) Name() string {
return "GitHubPullRequestDiff"
}

func (t *GitHubPullRequestDiffTool) Description() string {
return "Get the diff of a GitHub pull request"
}

func (t *GitHubPullRequestDiffTool) Icon() string {
return ""
}

func (t *GitHubPullRequestDiffTool) String() string {
return "GitHubPullRequestDiff"
}

func (t *GitHubPullRequestDiffTool) StatusMessage() string {
return "Getting GitHub pull request diff"
}

func (t *GitHubPullRequestDiffTool) OpenAI() []openai.Tool {
return []openai.Tool{
{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: "GitHubPullRequestDiff",
Description: pullRequestDiffSkillDescription,
Parameters: pullRequestDiffParameters,
},
},
}
}

func (t *GitHubPullRequestDiffTool) Execute(ctx context.Context, _ agent.Meta, _ map[string]interface{}) (string, error) {
ghCtx, ok := types.GetGitHubRepositoryContext(ctx)
if !ok {
return "", fmt.Errorf("github repository context not found")
}

client := NewClientWithPATAndBaseURL(t.token, t.baseURL)

pr, err := client.GetPullRequest(ctx, ghCtx.Owner, ghCtx.RepositoryName, ghCtx.PullRequestID)
if err != nil {
return "", fmt.Errorf("failed to get pull request: %w", err)
}

opts := &github.ListOptions{PerPage: 100}
files, _, err := client.client.PullRequests.ListFiles(ctx, ghCtx.Owner, ghCtx.RepositoryName, ghCtx.PullRequestID, opts)
if err != nil {
return "", fmt.Errorf("failed to list pull request files: %w", err)
}

var sb strings.Builder
sb.WriteString("Pull Request Details:\n")
sb.WriteString(fmt.Sprintf("Title: %s\n", pr.GetTitle()))
sb.WriteString(fmt.Sprintf("Description: %s\n", pr.GetBody()))
sb.WriteString(fmt.Sprintf("Author: %s\n", pr.GetUser().GetLogin()))
sb.WriteString(fmt.Sprintf("Head: %s (%s)\n", ghCtx.HeadBranch, ghCtx.HeadSHA))
sb.WriteString(fmt.Sprintf("Base: %s (%s)\n\n", ghCtx.BaseBranch, ghCtx.BaseSHA))

sb.WriteString(fmt.Sprintf("Changed files (%d):\n\n", len(files)))

for _, f := range files {
sb.WriteString(fmt.Sprintf("--- %s (%s, +%d -%d)\n", f.GetFilename(), f.GetStatus(), f.GetAdditions(), f.GetDeletions()))
if patch := f.GetPatch(); patch != "" {
sb.WriteString(patch)
sb.WriteString("\n\n")
}
}

return sb.String(), nil
}
6 changes: 6 additions & 0 deletions api/pkg/controller/inference_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
agent "github.com/helixml/helix/api/pkg/agent"
"github.com/helixml/helix/api/pkg/agent/skill"
azuredevops "github.com/helixml/helix/api/pkg/agent/skill/azure_devops"
githubskill "github.com/helixml/helix/api/pkg/agent/skill/github"
"github.com/helixml/helix/api/pkg/agent/skill/mcp"
"github.com/helixml/helix/api/pkg/agent/skill/memory"
"github.com/helixml/helix/api/pkg/agent/skill/project"
Expand Down Expand Up @@ -170,6 +171,11 @@ func (c *Controller) runAgent(ctx context.Context, req *runAgentRequest) (*agent
skills = append(skills, azuredevops.NewPullRequestDiffSkill(assistantTool.Config.AzureDevOps.OrganizationURL, assistantTool.Config.AzureDevOps.PersonalAccessToken))
}

if assistantTool.ToolType == types.ToolTypeGitHub {
skills = append(skills, githubskill.NewPullRequestDiffSkill(assistantTool.Config.GitHub.PersonalAccessToken, assistantTool.Config.GitHub.BaseURL))
skills = append(skills, githubskill.NewCreateReviewCommentSkill(assistantTool.Config.GitHub.PersonalAccessToken, assistantTool.Config.GitHub.BaseURL))
}

}

// Get assistant knowledge
Expand Down
39 changes: 39 additions & 0 deletions api/pkg/server/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20144,6 +20144,9 @@ const docTemplate = `{
"generation_model_provider": {
"type": "string"
},
"github": {
"$ref": "#/definitions/types.AssistantGitHub"
},
"id": {
"type": "string"
},
Expand Down Expand Up @@ -20286,6 +20289,20 @@ const docTemplate = `{
}
}
},
"types.AssistantGitHub": {
"type": "object",
"properties": {
"base_url": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"personal_access_token": {
"type": "string"
}
}
},
"types.AssistantKnowledge": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -20415,6 +20432,9 @@ const docTemplate = `{
"email": {
"$ref": "#/definitions/types.AssistantEmail"
},
"github": {
"$ref": "#/definitions/types.AssistantGitHub"
},
"mcps": {
"type": "array",
"items": {
Expand Down Expand Up @@ -29193,6 +29213,9 @@ const docTemplate = `{
"email": {
"$ref": "#/definitions/types.ToolEmailConfig"
},
"github": {
"$ref": "#/definitions/types.ToolGitHubConfig"
},
"mcp": {
"$ref": "#/definitions/types.ToolMCPClientConfig"
},
Expand Down Expand Up @@ -29223,6 +29246,20 @@ const docTemplate = `{
}
}
},
"types.ToolGitHubConfig": {
"type": "object",
"properties": {
"base_url": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"personal_access_token": {
"type": "string"
}
}
},
"types.ToolMCPClientConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -29287,6 +29324,7 @@ const docTemplate = `{
"email",
"web_search",
"azure_devops",
"github",
"mcp",
"project_manager"
],
Expand All @@ -29298,6 +29336,7 @@ const docTemplate = `{
"ToolTypeEmail",
"ToolTypeWebSearch",
"ToolTypeAzureDevOps",
"ToolTypeGitHub",
"ToolTypeMCP",
"ToolTypeProjectManager"
]
Expand Down
Loading