Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
kechigon committed Jan 6, 2025
2 parents 4481824 + 007fbfb commit 5ef028c
Show file tree
Hide file tree
Showing 13 changed files with 563 additions and 232 deletions.
2 changes: 1 addition & 1 deletion .alert-menta.user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ system:
ai:
provider: "openai" # "openai" or "vertexai"
openai:
model: "gpt-3.5-turbo" # Check the list of available models by `curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"`
model: "gpt-4o-mini-2024-07-18" # Check the list of available models by `curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"`

vertexai:
project: "<YOUR_PROJECT_ID>"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/alert-menta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
jobs:
Alert-Menta:
if: (startsWith(github.event.comment.body, '/describe') || startsWith(github.event.comment.body, '/suggest') || startsWith(github.event.comment.body, '/ask')) && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
issues: write
contents: read
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Go test

on:
push:
branches:
- develop
- main
pull_request:
branches:
- develop
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.2

- name: Install dependencies
run: go mod tidy

- name: Build
run: go build ./...

- name: Run tests
run: go test ./... -v
200 changes: 127 additions & 73 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,128 +2,182 @@ package main

import (
"flag"
"fmt"
"log"
"os"
"regexp"
"strings"

"github.com/3-shake/alert-menta/internal/ai"
"github.com/3-shake/alert-menta/internal/github"
"github.com/3-shake/alert-menta/internal/utils"
)

// Struct to hold the command-line arguments
type Config struct {
repo string
owner string
issueNumber int
intent string
command string
configFile string
ghToken string
oaiKey string
}

func main() {
// Get command line arguments
var (
repo = flag.String("repo", "", "Repository name")
owner = flag.String("owner", "", "Repository owner")
issueNumber = flag.Int("issue", 0, "Issue number")
intent = flag.String("intent", "", "Question or intent for the 'ask' command")
command = flag.String("command", "", "Commands to be executed by AI.Commands defined in the configuration file are available.")
configFile = flag.String("config", "", "Configuration file")
gh_token = flag.String("github-token", "", "GitHub token")
oai_key = flag.String("api-key", "", "OpenAI api key")
)
cfg := &Config{}
flag.StringVar(&cfg.repo, "repo", "", "Repository name")
flag.StringVar(&cfg.owner, "owner", "", "Repository owner")
flag.IntVar(&cfg.issueNumber, "issue", 0, "Issue number")
flag.StringVar(&cfg.intent, "intent", "", "Question or intent for the 'ask' command")
flag.StringVar(&cfg.command, "command", "", "Commands to be executed by AI. Commands defined in the configuration file are available.")
flag.StringVar(&cfg.configFile, "config", "", "Configuration file")
flag.StringVar(&cfg.ghToken, "github-token", "", "GitHub token")
flag.StringVar(&cfg.oaiKey, "api-key", "", "OpenAI api key")
flag.Parse()

if *repo == "" || *owner == "" || *issueNumber == 0 || *gh_token == "" || *command == "" || *configFile == "" {
if cfg.repo == "" || cfg.owner == "" || cfg.issueNumber == 0 || cfg.ghToken == "" || cfg.command == "" || cfg.configFile == "" {
flag.PrintDefaults()
os.Exit(1)
}

// Initialize a logger
logger := log.New(
os.Stdout, "[alert-menta main] ",
log.Ldate|log.Ltime|log.Llongfile|log.Lmsgprefix,
)

// Load configuration
cfg, err := utils.NewConfig(*configFile)
loadedcfg, err := utils.NewConfig(cfg.configFile)
if err != nil {
logger.Fatalf("Error loading config: %v", err)
}

err = validateCommand(cfg.command, loadedcfg)
if err != nil {
logger.Fatalf("Error creating comment: %s", err)
logger.Fatalf("Error validating command: %v", err)
}

// Validate command
if _, ok := cfg.Ai.Commands[*command]; !ok {
allowedCommands := make([]string, 0, len(cfg.Ai.Commands))
for cmd := range cfg.Ai.Commands {
allowedCommands = append(allowedCommands, cmd)
}
logger.Fatalf("Invalid command: %s. Allowed commands are %s.", *command, strings.Join(allowedCommands, ", "))
issue := github.NewIssue(cfg.owner, cfg.repo, cfg.issueNumber, cfg.ghToken)

userPrompt, imgs, err := constructUserPrompt(cfg.ghToken, issue, loadedcfg, logger)
if err != nil {
logger.Fatalf("Erro constructing userPrompt: %v", err)
}

prompt, err := constructPrompt(cfg.command, cfg.intent, userPrompt, imgs, loadedcfg, logger)
if err != nil {
logger.Fatalf("Error constructing prompt: %v", err)
}

// Create a GitHub Issues instance. From now on, you can control GitHub from this instance.
issue := github.NewIssue(*owner, *repo, *issueNumber, *gh_token)
if issue == nil {
logger.Fatalf("Failed to create GitHub issue instance")

aic, err := getAIClient(cfg.oaiKey, loadedcfg, logger)
if err != nil {
logger.Fatalf("Error geting AI client: %v", err)
}

comment, err := aic.GetResponse(prompt)
if err != nil {
logger.Fatalf("Error getting Response: %v", err)
}
logger.Println("Response:", comment)

if err := issue.PostComment(comment); err != nil {
logger.Fatalf("Error creating comment: %v", err)
}
}

// Validate the provided command
func validateCommand(command string, cfg *utils.Config) error {
if _, ok := cfg.Ai.Commands[command]; !ok {
allowedCommands := make([]string, 0, len(cfg.Ai.Commands))
for cmd := range cfg.Ai.Commands {
allowedCommands = append(allowedCommands, cmd)
}
return fmt.Errorf("Invalid command: %s. Allowed commands are %s", command, strings.Join(allowedCommands, ", "))
}
return nil
}

// Get Issue's information(e.g. Title, Body) and add them to the user prompt except for comments by Actions.
// Construct user prompt from issue
func constructUserPrompt(ghToken string, issue *github.GitHubIssue, cfg *utils.Config, logger *log.Logger) (string, []ai.Image, error) {
title, err := issue.GetTitle()
if err != nil {
logger.Fatalf("Error getting Title: %v", err)
return "", nil, fmt.Errorf("Error getting Title: %w", err)
}

body, err := issue.GetBody()
if err != nil {
logger.Fatalf("Error getting Body: %v", err)
}
if cfg.System.Debug.Log_level == "debug" {
logger.Println("Title:", *title)
logger.Println("Body:", *body)
return "", nil, fmt.Errorf("Error getting Body: %w", err)
}
user_prompt := "Title:" + *title + "\n"
user_prompt += "Body:" + *body + "\n"

// Get comments under the Issue and add them to the user prompt except for comments by Actions.
var userPrompt strings.Builder
userPrompt.WriteString("Title:" + *title + "\n")
userPrompt.WriteString("Body:" + *body + "\n")

comments, err := issue.GetComments()
if err != nil {
logger.Fatalf("Error getting comments: %v", err)
return "", nil, fmt.Errorf("Error getting comments: %w", err)
}

var images []ai.Image
imageRegex := regexp.MustCompile(`!\[(.*?)\]\((.*?)\)`)

for _, v := range comments {
if *v.User.Login == "github-actions[bot]" {
continue
}
if cfg.System.Debug.Log_level == "debug" {
logger.Printf("%s: %s", *v.User.Login, *v.Body)
}
user_prompt += *v.User.Login + ":" + *v.Body + "\n"
userPrompt.WriteString(*v.User.Login + ":" + *v.Body + "\n")

matches := imageRegex.FindAllStringSubmatch(*v.Body, -1)
for _, match := range matches {
logger.Println("Image URL:", match[2]) // Log the URL of the image
imgData, ext, err := utils.DownloadImage(match[2], ghToken)
if err != nil {
return "", nil, fmt.Errorf("Error downloading image: %w", err)
}

images = append(images, ai.Image{Data: imgData, Extension: ext})
}
}
return userPrompt.String(), images, nil
}

// Set system prompt
var system_prompt string
if *command == "ask" {
if *intent == "" {
logger.Fatalf("Error: intent is required for 'ask' command")
}
system_prompt = cfg.Ai.Commands[*command].System_prompt + *intent + "\n"
} else {
system_prompt = cfg.Ai.Commands[*command].System_prompt
}
prompt := ai.Prompt{UserPrompt: user_prompt, SystemPrompt: system_prompt}
logger.Println("\x1b[34mPrompt: |\n", prompt.SystemPrompt, prompt.UserPrompt, "\x1b[0m")

// Get response from OpenAI or VertexAI
var aic ai.Ai
if cfg.Ai.Provider == "openai" {
if *oai_key == "" {
logger.Fatalf("Error: Please provide your Open AI API key.")
}
aic = ai.NewOpenAIClient(*oai_key, cfg.Ai.OpenAI.Model)
// Construct AI prompt
func constructPrompt(command, intent, userPrompt string, imgs []ai.Image, cfg *utils.Config, logger *log.Logger) (*ai.Prompt, error) {
var systemPrompt string
if command == "ask" {
if intent == "" {
return nil, fmt.Errorf("Error: intent is required for 'ask' command")
}
systemPrompt = cfg.Ai.Commands[command].System_prompt + intent + "\n"
} else {
systemPrompt = cfg.Ai.Commands[command].System_prompt
}
logger.Println("\x1b[34mPrompt: |\n", systemPrompt, userPrompt, "\x1b[0m")
return &ai.Prompt{UserPrompt: userPrompt, SystemPrompt: systemPrompt, Images: imgs}, nil
}

// Initialize AI client
func getAIClient(oaiKey string, cfg *utils.Config, logger *log.Logger) (ai.Ai, error) {
switch cfg.Ai.Provider {
case "openai":
if oaiKey == "" {
return nil, fmt.Errorf("Error: Please provide your Open AI API key")
}
logger.Println("Using OpenAI API")
logger.Println("OpenAI model:", cfg.Ai.OpenAI.Model)
} else if cfg.Ai.Provider == "vertexai"{
aic = ai.NewVertexAIClient(cfg.Ai.VertexAI.Project, cfg.Ai.VertexAI.Region, cfg.Ai.VertexAI.Model)
return ai.NewOpenAIClient(oaiKey, cfg.Ai.OpenAI.Model), nil
case "vertexai":
logger.Println("Using VertexAI API")
logger.Println("VertexAI model:", cfg.Ai.VertexAI.Model)
} else {
logger.Fatalf("Error: Invalid provider")
}

comment, _ := aic.GetResponse(prompt)
logger.Println("Response:", comment)

// Post a comment on the Issue
err = issue.PostComment(comment)
if err != nil {
logger.Fatalf("Error creating comment: %s", err)
aic, err := ai.NewVertexAIClient(cfg.Ai.VertexAI.Project, cfg.Ai.VertexAI.Region, cfg.Ai.VertexAI.Model)
if err != nil {
return nil, fmt.Errorf("Error: new Vertex AI client: %w", err)
}
return aic, nil
default:
return nil, fmt.Errorf("Error: Invalid provider")
}
}
Loading

0 comments on commit 5ef028c

Please sign in to comment.