diff --git a/.alert-menta.user.yaml b/.alert-menta.user.yaml index 74664f3..392184e 100644 --- a/.alert-menta.user.yaml +++ b/.alert-menta.user.yaml @@ -10,15 +10,18 @@ ai: vertexai: project: "" location: "us-central1" - model: "gemini-1.5-flash-001" + model: "gemini-2.0-flash-001" commands: - describe: description: "Generate a detailed description of the Issue." system_prompt: "The following is the GitHub Issue and comments on it. Please Generate a detailed description.\n" + require_intent: false - suggest: description: "Provide suggestions for improvement based on the contents of the Issue." system_prompt: "The following is the GitHub Issue and comments on it. Please identify the issues that need to be resolved based on the contents of the Issue and provide three suggestions for improvement.\n" + require_intent: false - ask: description: "Answer free-text questions." system_prompt: "The following is the GitHub Issue and comments on it. Based on the content provide a detailed response to the following question:\n" + require_intent: true diff --git a/.github/workflows/alert-menta.yaml b/.github/workflows/alert-menta.yaml index 9002a8b..0e3f5fb 100644 --- a/.github/workflows/alert-menta.yaml +++ b/.github/workflows/alert-menta.yaml @@ -7,7 +7,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') + if: startsWith(github.event.comment.body, '/') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') runs-on: ubuntu-24.04 permissions: issues: write @@ -24,21 +24,6 @@ jobs: | jq '.assets[] | select(.name | contains("Linux_x86")) | .id')" tar -zxvf alert-menta_Linux_x86_64.tar.gz - - name: Set Command - id: set_command - run: | - COMMENT_BODY="${{ github.event.comment.body }}" - if [[ "$COMMENT_BODY" == /ask* ]]; then - COMMAND=ask - INTENT=${COMMENT_BODY:5} - echo "INTENT=$INTENT" >> $GITHUB_ENV - elif [[ "$COMMENT_BODY" == /describe* ]]; then - COMMAND=describe - elif [[ "$COMMENT_BODY" == /suggest* ]]; then - COMMAND=suggest - fi - echo "COMMAND=$COMMAND" >> $GITHUB_ENV - - run: echo "REPOSITORY_NAME=${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}" >> $GITHUB_ENV - name: Get user defined config file @@ -47,9 +32,27 @@ jobs: run: | curl -H "Authorization: token ${{ secrets.GH_TOKEN }}" -L -o .alert-menta.user.yaml "https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.REPOSITORY_NAME }}/main/.alert-menta.user.yaml" && echo "CONFIG_FILE=./.alert-menta.user.yaml" >> $GITHUB_ENV + - name: Extract command and intent + id: extract_command + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + COMMAND=$(echo "$COMMENT_BODY" | sed -E 's|^/([^ ]*).*|\1|') + echo "COMMAND=$COMMAND" >> $GITHUB_ENV + + if [[ "$COMMENT_BODY" == "/$COMMAND "* ]]; then + INTENT=$(echo "$COMMENT_BODY" | sed -E "s|^/$COMMAND ||") + echo "INTENT=$INTENT" >> $GITHUB_ENV + fi + + COMMANDS_CHECK=$(yq e '.ai.commands[] | keys' .alert-menta.user.yaml | grep -c "$COMMAND" || echo "0") + if [ "$COMMANDS_CHECK" -eq "0" ]; then + echo "Invalid command: $COMMAND. Command not found in configuration." + exit 1 + fi + - name: Add Comment run: | - if [[ "$COMMAND" == "ask" ]]; then + if [ -n "$INTENT" ]; then ./alert-menta -owner ${{ github.repository_owner }} -issue ${{ github.event.issue.number }} -repo ${{ env.REPOSITORY_NAME }} -github-token ${{ secrets.GH_TOKEN }} -api-key ${{ secrets.OPENAI_API_KEY }} -command $COMMAND -config $CONFIG_FILE -intent "$INTENT" else ./alert-menta -owner ${{ github.repository_owner }} -issue ${{ github.event.issue.number }} -repo ${{ env.REPOSITORY_NAME }} -github-token ${{ secrets.GH_TOKEN }} -api-key ${{ secrets.OPENAI_API_KEY }} -command $COMMAND -config $CONFIG_FILE diff --git a/README.md b/README.md index a77170f..163e18a 100644 --- a/README.md +++ b/README.md @@ -48,24 +48,42 @@ 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-40-mini" # Check the list of available models by curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY" vertexai: project: "" location: "us-central1" - model: "gemini-1.5-flash-001" + model: "gemini-2.0-flash-001" commands: - describe: description: "Generate a detailed description of the Issue." system_prompt: "The following is the GitHub Issue and comments on it. Please Generate a detailed description.\n" + require_intent: false - suggest: description: "Provide suggestions for improvement based on the contents of the Issue." system_prompt: "The following is the GitHub Issue and comments on it. Please identify the issues that need to be resolved based on the contents of the Issue and provide three suggestions for improvement.\n" + require_intent: false - ask: description: "Answer free-text questions." system_prompt: "The following is the GitHub Issue and comments on it. Based on the content, provide a detailed response to the following question:\n" + require_intent: true ``` Specify the LLM to use with `ai.provider`. You can change the system prompt with `commands.{command}.system_prompt`. +#### Custom command +`.alert-menta.user.yaml` allows you to set up custom commands for users. +Set the following in `command.{command}`. +- `description` +- `system_prompt`: describe the primary instructions for this command. +- `require_intent`: allows the command to specify arguments. (e.g. if `require_intent` is true, we execute command that `/{command} “some instruction”`) + +As an example, to configure the ANALYSIS command, write: +```yaml +- analysis: + description: "This command performs a root cause analysis of an Issue." + system_prompt: "Please determine the root cause of the issue and resolve it based on the content of the issue." + require_intent: false +``` + ### Actions #### Template The `.github/workflows/alert-menta.yaml` in this repository is a template. The contents are as follows: @@ -79,8 +97,8 @@ 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 + if: startsWith(github.event.comment.body, '/') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') + runs-on: ubuntu-24.04 permissions: issues: write contents: read @@ -96,21 +114,6 @@ jobs: | jq '.assets[] | select(.name | contains("Linux_x86")) | .id')" tar -zxvf alert-menta_Linux_x86_64.tar.gz - - name: Set Command - id: set_command - run: | - COMMENT_BODY="${{ github.event.comment.body }}" - if [[ "$COMMENT_BODY" == /ask* ]]; then - COMMAND=ask - INTENT=${COMMENT_BODY:5} - echo "INTENT=$INTENT" >> $GITHUB_ENV - elif [[ "$COMMENT_BODY" == /describe* ]]; then - COMMAND=describe - elif [[ "$COMMENT_BODY" == /suggest* ]]; then - COMMAND=suggest - fi - echo "COMMAND=$COMMAND" >> $GITHUB_ENV - - run: echo "REPOSITORY_NAME=${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}" >> $GITHUB_ENV - name: Get user defined config file @@ -119,9 +122,27 @@ jobs: run: | curl -H "Authorization: token ${{ secrets.GH_TOKEN }}" -L -o .alert-menta.user.yaml "https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.REPOSITORY_NAME }}/main/.alert-menta.user.yaml" && echo "CONFIG_FILE=./.alert-menta.user.yaml" >> $GITHUB_ENV + - name: Extract command and intent + id: extract_command + run: | + COMMENT_BODY="${{ github.event.comment.body }}" + COMMAND=$(echo "$COMMENT_BODY" | sed -E 's|^/([^ ]*).*|\1|') + echo "COMMAND=$COMMAND" >> $GITHUB_ENV + + if [[ "$COMMENT_BODY" == "/$COMMAND "* ]]; then + INTENT=$(echo "$COMMENT_BODY" | sed -E "s|^/$COMMAND ||") + echo "INTENT=$INTENT" >> $GITHUB_ENV + fi + + COMMANDS_CHECK=$(yq e '.ai.commands[] | keys' .alert-menta.user.yaml | grep -c "$COMMAND" || echo "0") + if [ "$COMMANDS_CHECK" -eq "0" ]; then + echo "Invalid command: $COMMAND. Command not found in configuration." + exit 1 + fi + - name: Add Comment run: | - if [[ "$COMMAND" == "ask" ]]; then + if [ -n "$INTENT" ]; then ./alert-menta -owner ${{ github.repository_owner }} -issue ${{ github.event.issue.number }} -repo ${{ env.REPOSITORY_NAME }} -github-token ${{ secrets.GH_TOKEN }} -api-key ${{ secrets.OPENAI_API_KEY }} -command $COMMAND -config $CONFIG_FILE -intent "$INTENT" else ./alert-menta -owner ${{ github.repository_owner }} -issue ${{ github.event.issue.number }} -repo ${{ env.REPOSITORY_NAME }} -github-token ${{ secrets.GH_TOKEN }} -api-key ${{ secrets.OPENAI_API_KEY }} -command $COMMAND -config $CONFIG_FILE diff --git a/cmd/main.go b/cmd/main.go index 010d151..846dde7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -52,12 +52,48 @@ func main() { logger.Fatalf("Error loading config: %v", err) } + issue := github.NewIssue(cfg.owner, cfg.repo, cfg.issueNumber, cfg.ghToken) + + // Validate command err = validateCommand(cfg.command, loadedcfg) if err != nil { - logger.Fatalf("Error validating command: %v", err) + // Get available commands for the error message + availableCommands := getAvailableCommands(loadedcfg) + usageMessage := fmt.Sprintf("**Error**: %v\n\n**Available commands:**\n", err) + + // Add each command with its description to the usage message + for cmd, description := range availableCommands { + usageMessage += fmt.Sprintf("- `/%s`: %s\n", cmd, description) + } + + // Post the usage message as a comment + if postErr := issue.PostComment(usageMessage); postErr != nil { + logger.Fatalf("Error posting error comment: %v", postErr) + } + + // Exit with error code + logger.Printf("Error validating command: %v", err) + os.Exit(1) } - issue := github.NewIssue(cfg.owner, cfg.repo, cfg.issueNumber, cfg.ghToken) + // Check if intent is required for this command and missing + needsIntent, err := commandNeedsIntent(cfg.command, loadedcfg) + if err != nil { + logger.Fatalf("Error checking if intent is required: %v", err) + } + if needsIntent && cfg.intent == "" { + usageMessage := fmt.Sprintf("**Error**: The `/%s` command requires additional text after the command.\n\n**Usage**: `/%s [your text here]`", + cfg.command, cfg.command) + + // Post the usage message as a comment + if postErr := issue.PostComment(usageMessage); postErr != nil { + logger.Fatalf("Error posting error comment: %v", postErr) + } + + // Exit with error code + logger.Printf("Error: intent required for command %s", cfg.command) + os.Exit(1) + } userPrompt, imgs, err := constructUserPrompt(cfg.ghToken, issue, loadedcfg, logger) if err != nil { @@ -97,6 +133,27 @@ func validateCommand(command string, cfg *utils.Config) error { return nil } +// Check if a command requires an intent +func commandNeedsIntent(command string, cfg *utils.Config) (bool, error) { + // Get the command configuration + cmd, ok := cfg.Ai.Commands[command] + if !ok { + return false, fmt.Errorf("Command not found: %s", command) + } + + // Check if this command requires intent + return cmd.Require_intent, nil +} + +// Get available commands with descriptions for usage message +func getAvailableCommands(cfg *utils.Config) map[string]string { + commands := make(map[string]string) + for cmd, cmdConfig := range cfg.Ai.Commands { + commands[cmd] = cmdConfig.Description + } + return commands +} + // 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() @@ -147,9 +204,9 @@ func constructUserPrompt(ghToken string, issue *github.GitHubIssue, cfg *utils.C // 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 cfg.Ai.Commands[command].Require_intent { if intent == "" { - return nil, fmt.Errorf("Error: intent is required for 'ask' command") + return nil, fmt.Errorf("Error: intent is required for '%s' command", command) } systemPrompt = cfg.Ai.Commands[command].System_prompt + intent + "\n" } else { diff --git a/cmd/main_test.go b/cmd/main_test.go index 61ffa2b..77f08d5 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -41,8 +41,8 @@ func TestConstructPrompt(t *testing.T) { mockCfg := &utils.Config{ Ai: utils.Ai{ Commands: map[string]utils.Command{ - "ask": {System_prompt: "Ask system prompt: "}, - "other": {System_prompt: "Other system prompt: "}, + "ask": {System_prompt: "Ask system prompt: ", Require_intent: true}, + "other": {System_prompt: "Other system prompt: ", Require_intent: false}, }, }, } @@ -111,3 +111,55 @@ func TestGetAIClient(t *testing.T) { } } } + +// Test for getAvailableCommands +func TestGetAvailableCommands(t *testing.T) { + mockCfg := &utils.Config{ + Ai: utils.Ai{ + Commands: map[string]utils.Command{ + "valid": {Description: "Valid command"}, + "other": {Description: "Other command"}, + }, + }, + } + commands := getAvailableCommands(mockCfg) + if len(commands) != 2 { + t.Errorf("expected 2 commands, got %d", len(commands)) + } +} + +// Test for commandNeedsIntent +func TestCommandNeedsIntent(t *testing.T) { + mockCfg := &utils.Config{ + Ai: utils.Ai{ + Commands: map[string]utils.Command{ + "ask": {System_prompt: "Ask system prompt: ", Require_intent: true}, + "other": {System_prompt: "Other system prompt: ", Require_intent: false}, + }, + }, + } + + tests := []struct { + command string + shouldNeed bool + expectError bool + }{ + {"ask", true, false}, + {"other", false, false}, + {"nonexistent", false, true}, + } + + for _, tt := range tests { + needsIntent, err := commandNeedsIntent(tt.command, mockCfg) + + // Check if error condition matches expectation + if (err != nil) != tt.expectError { + t.Errorf("expected error: %v, got: %v for command %s", tt.expectError, err != nil, tt.command) + } + + // If not expecting an error, check the result + if !tt.expectError && needsIntent != tt.shouldNeed { + t.Errorf("expected %v for command %s, got %v", tt.shouldNeed, tt.command, needsIntent) + } + } +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index e4c5231..510d5ea 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -36,8 +36,9 @@ type Ai struct { } type Command struct { - Description string `yaml:"description"` - System_prompt string `yaml:"system_prompt"` + Description string `yaml:"description"` + System_prompt string `yaml:"system_prompt"` + Require_intent bool `yaml:"require_intent"` } type OpenAI struct {