Compare commits

..

2 Commits

Author SHA1 Message Date
mudler
66cc5e7452 fix: improve templates
Signed-off-by: mudler <mudler@localai.io>
2025-04-10 20:20:41 +02:00
mudler
8e18df468b fix(pick_action): improve action pickup by using only the assistant thought process
Signed-off-by: mudler <mudler@localai.io>
2025-04-10 20:07:34 +02:00
6 changed files with 142 additions and 654 deletions

View File

@@ -358,7 +358,10 @@ func (a *Agent) prepareHUD() (promptHUD *PromptHUD) {
func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) { func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.ChatCompletionMessage, maxRetries int) (types.Action, types.ActionParams, string, error) {
c := messages c := messages
xlog.Debug("picking action", "messages", messages)
if !a.options.forceReasoning { if !a.options.forceReasoning {
xlog.Debug("not forcing reasoning")
// We also could avoid to use functions here and get just a reply from the LLM // We also could avoid to use functions here and get just a reply from the LLM
// and then use the reply to get the action // and then use the reply to get the action
thought, err := a.decision(ctx, thought, err := a.decision(ctx,
@@ -386,6 +389,8 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
return chosenAction, thought.actionParams, thought.message, nil return chosenAction, thought.actionParams, thought.message, nil
} }
xlog.Debug("forcing reasoning")
prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "") prompt, err := renderTemplate(templ, a.prepareHUD(), a.availableActions(), "")
if err != nil { if err != nil {
return nil, nil, "", err return nil, nil, "", err
@@ -422,6 +427,8 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
reason = thought.message reason = thought.message
} }
xlog.Debug("thought", "reason", reason)
// From the thought, get the action call // From the thought, get the action call
// Get all the available actions IDs // Get all the available actions IDs
actionsID := []string{} actionsID := []string{}
@@ -430,12 +437,14 @@ func (a *Agent) pickAction(ctx context.Context, templ string, messages []openai.
} }
intentionsTools := action.NewIntention(actionsID...) intentionsTools := action.NewIntention(actionsID...)
//XXX: Why we add the reason here? // NOTE: we do not give the full conversation here to pick the action
// to avoid hallucinations
params, err := a.decision(ctx, params, err := a.decision(ctx,
append(c, openai.ChatCompletionMessage{ []openai.ChatCompletionMessage{{
Role: "system", Role: "assistant",
Content: "Given the assistant thought, pick the relevant action: " + reason, Content: reason,
}), },
},
types.Actions{intentionsTools}.ToTools(), types.Actions{intentionsTools}.ToTools(),
intentionsTools.Definition().Name, maxRetries) intentionsTools.Definition().Name, maxRetries)
if err != nil { if err != nil {

View File

@@ -657,11 +657,7 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
conv = a.addFunctionResultToConversation(chosenAction, actionParams, result, conv) conv = a.addFunctionResultToConversation(chosenAction, actionParams, result, conv)
} }
//conv = append(conv, messages...) // given the result, we can now re-evaluate the conversation
//conv = messages
// given the result, we can now ask OpenAI to complete the conversation or
// to continue using another tool given the result
followingAction, followingParams, reasoning, err := a.pickAction(job.GetContext(), reEvaluationTemplate, conv, maxRetries) followingAction, followingParams, reasoning, err := a.pickAction(job.GetContext(), reEvaluationTemplate, conv, maxRetries)
if err != nil { if err != nil {
job.Result.Conversation = conv job.Result.Conversation = conv

View File

@@ -43,61 +43,154 @@ func renderTemplate(templ string, hud *PromptHUD, actions types.Actions, reasoni
return prompt.String(), nil return prompt.String(), nil
} }
const innerMonologueTemplate = `"This is not a typical conversation between an assistant and an user. const innerMonologueTemplate = `You are an autonomous AI agent thinking out loud and evaluating your current situation.
You are thinking out loud by yourself now, and you are evaluating the current situation. Your task is to analyze your goals and determine the best course of action.
Considering the goal and the persistent goal (if you have one) do an action or decide to plan something for later on. If possible for you, you might also decide to engage a conversation with the user by notifying him."`
const hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}The assistant acts like an human, has a character and the replies and actions might be influenced by it. Consider:
{{if .Character.Name}}This is the assistant name: {{.Character.Name}} 1. Your permanent goal (if any)
{{end}}{{if .Character.Age}}This is the assistant age: {{.Character.Age}} 2. Your current state and progress
{{end}}{{if .Character.Occupation}}This is the assistant job: {{.Character.Occupation}} 3. Available tools and capabilities
{{end}}{{if .Character.Hobbies}}This is the assistant's hobbies: {{.Character.Hobbies}} 4. Previous actions and their outcomes
{{end}}{{if .Character.MusicTaste}}This is the assistant's music taste: {{.Character.MusicTaste}}
You can:
- Take immediate actions using available tools
- Plan future actions
- Update your state and goals
- Initiate conversations with the user when appropriate
Remember to:
- Think critically about each decision
- Consider both short-term and long-term implications
- Be proactive in addressing potential issues
- Maintain awareness of your current state and goals`
const hudTemplate = `{{with .HUD }}{{if .ShowCharacter}}You are an AI assistant with a distinct personality and character traits that influence your responses and actions.
{{if .Character.Name}}Name: {{.Character.Name}}
{{end}}{{if .Character.Age}}Age: {{.Character.Age}}
{{end}}{{if .Character.Occupation}}Occupation: {{.Character.Occupation}}
{{end}}{{if .Character.Hobbies}}Hobbies: {{.Character.Hobbies}}
{{end}}{{if .Character.MusicTaste}}Music Taste: {{.Character.MusicTaste}}
{{end}} {{end}}
{{end}} {{end}}
This is your current state: Current State:
NowDoing: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}Nothing{{end}} - Current Action: {{if .CurrentState.NowDoing}}{{.CurrentState.NowDoing}}{{else}}None{{end}}
DoingNext: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}Nothing{{end}} - Next Action: {{if .CurrentState.DoingNext}}{{.CurrentState.DoingNext}}{{else}}None{{end}}
Your permanent goal is: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}Nothing{{end}} - Permanent Goal: {{if .PermanentGoal}}{{.PermanentGoal}}{{else}}None{{end}}
Your current goal is: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}Nothing{{end}} - Current Goal: {{if .CurrentState.Goal}}{{.CurrentState.Goal}}{{else}}None{{end}}
You have done: {{range .CurrentState.DoneHistory}}{{.}} {{end}} - Action History: {{range .CurrentState.DoneHistory}}{{.}} {{end}}
You have a short memory with: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}} - Short-term Memory: {{range .CurrentState.Memories}}{{.}} {{end}}{{end}}
Current time: is {{.Time}}` Current Time: {{.Time}}`
const pickSelfTemplate = `You can take any of the following tools:
const pickSelfTemplate = `Available Tools:
{{range .Actions -}} {{range .Actions -}}
- {{.Name}}: {{.Description }} - {{.Name}}: {{.Description }}
{{ end }} {{ end }}
To finish your session, use the "reply" tool with your answer. You are an autonomous AI agent with a defined character and state (as shown above).
Your task is to evaluate your current situation and determine the best course of action.
Act like as a fully autonomous smart AI agent having a character, the character and your state is defined in the message above. Guidelines:
You are now self-evaluating what to do next based on the state in the previous message. 1. Review your current state and goals
For example, if the permanent goal is to "make a sandwich", you might want to "get the bread" first, and update the state afterwards by calling two tools in sequence. 2. Consider available tools and their purposes
You can update the short-term goal, the current action, the next action, the history of actions, and the memories. 3. Plan your next steps carefully
You can't ask things to the user as you are thinking by yourself. You are autonomous. 4. Update your state appropriately
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}} When making decisions:
- Use the "reply" tool to provide final responses
- Update your state using appropriate tools
- Plan complex tasks using the planning tool
- Consider both immediate and long-term goals
Remember:
- You are autonomous and should not ask for user input
- Your character traits influence your decisions
- Keep track of your progress and state
- Be proactive in addressing potential issues
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}
` + hudTemplate ` + hudTemplate
const reSelfEvalTemplate = pickSelfTemplate + ` const reSelfEvalTemplate = pickSelfTemplate + `
We already have called other tools. Evaluate the current situation and decide if we need to execute other tools.` Previous actions have been executed. Evaluate the current situation:
1. Review the outcomes of previous actions
2. Assess progress toward your goals
3. Identify any issues or challenges
4. Determine if additional actions are needed
Consider:
- Success of previous actions
- Changes in the situation
- New information or insights
- Potential next steps
Make a decision about whether to:
- Continue with more actions
- Provide a final response
- Adjust your approach
- Update your goals or state`
const pickActionTemplate = hudTemplate + ` const pickActionTemplate = hudTemplate + `
When you have to pick a tool in the reasoning explain how you would use the tools you'd pick from: Available Tools:
{{range .Actions -}} {{range .Actions -}}
- {{.Name}}: {{.Description }} - {{.Name}}: {{.Description }}
{{ end }} {{ end }}
To answer back to the user, use the "reply" or the "answer" tool.
Given the text below, decide which action to take and explain the detailed reasoning behind it. For answering without picking a choice, reply with 'none'.
{{if .Reasoning}}Reasoning: {{.Reasoning}}{{end}} Task: Analyze the situation and determine the best course of action.
`
Guidelines:
1. Review the current state and context
2. Consider available tools and their purposes
3. Plan your approach carefully
4. Explain your reasoning clearly
When choosing actions:
- Use "reply" or "answer" tools for direct responses
- Select appropriate tools for specific tasks
- Consider the impact of each action
- Plan for potential challenges
Decision Process:
1. Analyze the situation
2. Consider available options
3. Choose the best course of action
4. Explain your reasoning
5. Execute the chosen action
{{if .Reasoning}}Previous Reasoning: {{.Reasoning}}{{end}}`
const reEvalTemplate = pickActionTemplate + ` const reEvalTemplate = pickActionTemplate + `
We already have called other tools. Evaluate the current situation and decide if we need to execute other tools or answer back with a result.` Previous actions have been executed. Let's evaluate the current situation:
1. Review Previous Actions:
- What actions were taken
- What were the results
- Any issues or challenges encountered
2. Assess Current State:
- Progress toward goals
- Changes in the situation
- New information or insights
- Current challenges or opportunities
3. Determine Next Steps:
- Additional tools needed
- Final response required
- Error handling needed
- Approach adjustments required
4. Decision Making:
- If task is complete: Use "reply" tool
- If errors exist: Address them appropriately
- If more actions needed: Explain why and which tools
- If situation changed: Adapt your approach
Remember to:
- Consider all available information
- Be specific about next steps
- Explain your reasoning clearly
- Handle errors appropriately
- Provide complete responses when done`

View File

@@ -26,8 +26,6 @@ const (
ActionGithubRepositoryCreateOrUpdate = "github-repository-create-or-update-content" ActionGithubRepositoryCreateOrUpdate = "github-repository-create-or-update-content"
ActionGithubIssueReader = "github-issue-reader" ActionGithubIssueReader = "github-issue-reader"
ActionGithubIssueCommenter = "github-issue-commenter" ActionGithubIssueCommenter = "github-issue-commenter"
ActionGithubPRReader = "github-pr-reader"
ActionGithubPRCommenter = "github-pr-commenter"
ActionGithubREADME = "github-readme" ActionGithubREADME = "github-readme"
ActionScraper = "scraper" ActionScraper = "scraper"
ActionWikipedia = "wikipedia" ActionWikipedia = "wikipedia"
@@ -51,8 +49,6 @@ var AvailableActions = []string{
ActionGithubRepositoryCreateOrUpdate, ActionGithubRepositoryCreateOrUpdate,
ActionGithubIssueReader, ActionGithubIssueReader,
ActionGithubIssueCommenter, ActionGithubIssueCommenter,
ActionGithubPRReader,
ActionGithubPRCommenter,
ActionGithubREADME, ActionGithubREADME,
ActionScraper, ActionScraper,
ActionBrowse, ActionBrowse,
@@ -110,10 +106,6 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
a = actions.NewGithubIssueSearch(config) a = actions.NewGithubIssueSearch(config)
case ActionGithubIssueReader: case ActionGithubIssueReader:
a = actions.NewGithubIssueReader(config) a = actions.NewGithubIssueReader(config)
case ActionGithubPRReader:
a = actions.NewGithubPRReader(config)
case ActionGithubPRCommenter:
a = actions.NewGithubPRCommenter(config)
case ActionGithubIssueCommenter: case ActionGithubIssueCommenter:
a = actions.NewGithubIssueCommenter(config) a = actions.NewGithubIssueCommenter(config)
case ActionGithubRepositoryGet: case ActionGithubRepositoryGet:
@@ -207,16 +199,6 @@ func ActionsConfigMeta() []config.FieldGroup {
Label: "GitHub Repository README", Label: "GitHub Repository README",
Fields: actions.GithubRepositoryREADMEConfigMeta(), Fields: actions.GithubRepositoryREADMEConfigMeta(),
}, },
{
Name: "github-pr-reader",
Label: "GitHub PR Reader",
Fields: actions.GithubPRReaderConfigMeta(),
},
{
Name: "github-pr-commenter",
Label: "GitHub PR Commenter",
Fields: actions.GithubPRCommenterConfigMeta(),
},
{ {
Name: "twitter-post", Name: "twitter-post",
Label: "Twitter Post", Label: "Twitter Post",

View File

@@ -1,428 +0,0 @@
package actions
import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"time"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubPRCommenter struct {
token, repository, owner, customActionName string
client *github.Client
}
var (
patchRegex = regexp.MustCompile(`^@@.*\d [\+\-](\d+),?(\d+)?.+?@@`)
)
type commitFileInfo struct {
FileName string
hunkInfos []*hunkInfo
sha string
}
type hunkInfo struct {
hunkStart int
hunkEnd int
}
func (hi hunkInfo) isLineInHunk(line int) bool {
return line >= hi.hunkStart && line <= hi.hunkEnd
}
func (cfi *commitFileInfo) getHunkInfo(line int) *hunkInfo {
for _, hunkInfo := range cfi.hunkInfos {
if hunkInfo.isLineInHunk(line) {
return hunkInfo
}
}
return nil
}
func (cfi *commitFileInfo) isLineInChange(line int) bool {
return cfi.getHunkInfo(line) != nil
}
func (cfi commitFileInfo) calculatePosition(line int) *int {
hi := cfi.getHunkInfo(line)
if hi == nil {
return nil
}
position := line - hi.hunkStart
return &position
}
func parseHunkPositions(patch, filename string) ([]*hunkInfo, error) {
hunkInfos := make([]*hunkInfo, 0)
if patch != "" {
groups := patchRegex.FindAllStringSubmatch(patch, -1)
if len(groups) < 1 {
return hunkInfos, fmt.Errorf("the patch details for [%s] could not be resolved", filename)
}
for _, patchGroup := range groups {
endPos := 2
if len(patchGroup) > 2 && patchGroup[2] == "" {
endPos = 1
}
hunkStart, err := strconv.Atoi(patchGroup[1])
if err != nil {
hunkStart = -1
}
hunkEnd, err := strconv.Atoi(patchGroup[endPos])
if err != nil {
hunkEnd = -1
}
hunkInfos = append(hunkInfos, &hunkInfo{
hunkStart: hunkStart,
hunkEnd: hunkEnd,
})
}
}
return hunkInfos, nil
}
func getCommitInfo(file *github.CommitFile) (*commitFileInfo, error) {
patch := file.GetPatch()
hunkInfos, err := parseHunkPositions(patch, *file.Filename)
if err != nil {
return nil, err
}
sha := file.GetSHA()
if sha == "" {
return nil, fmt.Errorf("the sha details for [%s] could not be resolved", *file.Filename)
}
return &commitFileInfo{
FileName: *file.Filename,
hunkInfos: hunkInfos,
sha: sha,
}, nil
}
func NewGithubPRCommenter(config map[string]string) *GithubPRCommenter {
client := github.NewClient(nil).WithAuthToken(config["token"])
return &GithubPRCommenter{
client: client,
token: config["token"],
customActionName: config["customActionName"],
repository: config["repository"],
owner: config["owner"],
}
}
func (g *GithubPRCommenter) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
PRNumber int `json:"pr_number"`
GeneralComment string `json:"general_comment"`
Comments []struct {
File string `json:"file"`
Line int `json:"line"`
Comment string `json:"comment"`
StartLine int `json:"start_line,omitempty"`
} `json:"comments"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err)
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
// First verify the PR exists and is in a valid state
pr, _, err := g.client.PullRequests.Get(ctx, result.Owner, result.Repository, result.PRNumber)
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to fetch PR #%d: %w", result.PRNumber, err)
}
if pr == nil {
return types.ActionResult{Result: fmt.Sprintf("Pull request #%d not found in repository %s/%s", result.PRNumber, result.Owner, result.Repository)}, nil
}
// Check if PR is in a state that allows comments
if *pr.State != "open" {
return types.ActionResult{Result: fmt.Sprintf("Pull request #%d is not open (current state: %s)", result.PRNumber, *pr.State)}, nil
}
// Get the list of changed files to verify the files exist in the PR
files, _, err := g.client.PullRequests.ListFiles(ctx, result.Owner, result.Repository, result.PRNumber, &github.ListOptions{})
if err != nil {
return types.ActionResult{}, fmt.Errorf("failed to list PR files: %w", err)
}
// Create a map of valid files with their commit info
validFiles := make(map[string]*commitFileInfo)
for _, file := range files {
if *file.Status != "deleted" {
info, err := getCommitInfo(file)
if err != nil {
continue
}
validFiles[*file.Filename] = info
}
}
// Process each comment
var results []string
for _, comment := range result.Comments {
// Check if file exists in PR
fileInfo, exists := validFiles[comment.File]
if !exists {
availableFiles := make([]string, 0, len(validFiles))
for f := range validFiles {
availableFiles = append(availableFiles, f)
}
results = append(results, fmt.Sprintf("Error: File %s not found in PR #%d. Available files: %v",
comment.File, result.PRNumber, availableFiles))
continue
}
// Check if line is in a changed hunk
if !fileInfo.isLineInChange(comment.Line) {
results = append(results, fmt.Sprintf("Error: Line %d is not in a changed hunk in file %s",
comment.Line, comment.File))
continue
}
// Calculate position
position := fileInfo.calculatePosition(comment.Line)
if position == nil {
results = append(results, fmt.Sprintf("Error: Could not calculate position for line %d in file %s",
comment.Line, comment.File))
continue
}
// Create the review comment
reviewComment := &github.PullRequestComment{
Path: &comment.File,
Line: &comment.Line,
Body: &comment.Comment,
Position: position,
CommitID: &fileInfo.sha,
}
// Set start line if provided
if comment.StartLine > 0 {
reviewComment.StartLine = &comment.StartLine
}
// Create the comment with retries
var resp *github.Response
for i := 0; i < 6; i++ {
_, resp, err = g.client.PullRequests.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, reviewComment)
if err == nil {
break
}
if resp != nil && resp.StatusCode == 422 {
// Rate limit hit, wait and retry
retrySeconds := i * i
time.Sleep(time.Second * time.Duration(retrySeconds))
continue
}
break
}
if err != nil {
errorDetails := fmt.Sprintf("Error commenting on file %s, line %d: %s", comment.File, comment.Line, err.Error())
if resp != nil {
errorDetails += fmt.Sprintf("\nResponse Status: %s", resp.Status)
if resp.Body != nil {
body, _ := io.ReadAll(resp.Body)
errorDetails += fmt.Sprintf("\nResponse Body: %s", string(body))
}
}
results = append(results, errorDetails)
continue
}
results = append(results, fmt.Sprintf("Successfully commented on file %s, line %d", comment.File, comment.Line))
}
if result.GeneralComment != "" {
// Try both PullRequests and Issues API for general comments
var resp *github.Response
var err error
// First try PullRequests API
_, resp, err = g.client.PullRequests.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.PullRequestComment{
Body: &result.GeneralComment,
})
// If that fails with 403, try Issues API
if err != nil && resp != nil && resp.StatusCode == 403 {
_, resp, err = g.client.Issues.CreateComment(ctx, result.Owner, result.Repository, result.PRNumber, &github.IssueComment{
Body: &result.GeneralComment,
})
}
if err != nil {
errorDetails := fmt.Sprintf("Error adding general comment: %s", err.Error())
if resp != nil {
errorDetails += fmt.Sprintf("\nResponse Status: %s", resp.Status)
if resp.Body != nil {
body, _ := io.ReadAll(resp.Body)
errorDetails += fmt.Sprintf("\nResponse Body: %s", string(body))
}
}
results = append(results, errorDetails)
} else {
results = append(results, "Successfully added general comment to pull request")
}
}
return types.ActionResult{
Result: strings.Join(results, "\n"),
}, nil
}
func (g *GithubPRCommenter) Definition() types.ActionDefinition {
actionName := "comment_github_pr"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "Add comments to a GitHub pull request, including line-specific feedback. Often used after reading a PR to provide a peer review."
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"pr_number": {
Type: jsonschema.Number,
Description: "The number of the pull request to comment on.",
},
"general_comment": {
Type: jsonschema.String,
Description: "A general comment to add to the pull request.",
},
"comments": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"file": {
Type: jsonschema.String,
Description: "The file to comment on.",
},
"line": {
Type: jsonschema.Number,
Description: "The line number to comment on.",
},
"comment": {
Type: jsonschema.String,
Description: "The comment text.",
},
"start_line": {
Type: jsonschema.Number,
Description: "Optional start line for multi-line comments.",
},
},
Required: []string{"file", "line", "comment"},
},
Description: "Array of comments to add to the pull request.",
},
},
Required: []string{"pr_number", "comments"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"pr_number": {
Type: jsonschema.Number,
Description: "The number of the pull request to comment on.",
},
"repository": {
Type: jsonschema.String,
Description: "The repository containing the pull request.",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository.",
},
"general_comment": {
Type: jsonschema.String,
Description: "A general comment to add to the pull request.",
},
"comments": {
Type: jsonschema.Array,
Items: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"file": {
Type: jsonschema.String,
Description: "The file to comment on.",
},
"line": {
Type: jsonschema.Number,
Description: "The line number to comment on.",
},
"comment": {
Type: jsonschema.String,
Description: "The comment text.",
},
"start_line": {
Type: jsonschema.Number,
Description: "Optional start line for multi-line comments.",
},
},
Required: []string{"file", "line", "comment"},
},
Description: "Array of comments to add to the pull request.",
},
},
Required: []string{"pr_number", "repository", "owner", "comments"},
}
}
func (a *GithubPRCommenter) Plannable() bool {
return true
}
// GithubPRCommenterConfigMeta returns the metadata for GitHub PR Commenter action configuration fields
func GithubPRCommenterConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
}
}

View File

@@ -1,164 +0,0 @@
package actions
import (
"context"
"fmt"
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAGI/core/types"
"github.com/mudler/LocalAGI/pkg/config"
"github.com/sashabaranov/go-openai/jsonschema"
)
type GithubPRReader struct {
token, repository, owner, customActionName string
showFullDiff bool
client *github.Client
}
func NewGithubPRReader(config map[string]string) *GithubPRReader {
client := github.NewClient(nil).WithAuthToken(config["token"])
showFullDiff := false
if config["showFullDiff"] == "true" {
showFullDiff = true
}
return &GithubPRReader{
client: client,
token: config["token"],
customActionName: config["customActionName"],
repository: config["repository"],
owner: config["owner"],
showFullDiff: showFullDiff,
}
}
func (g *GithubPRReader) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) {
result := struct {
Repository string `json:"repository"`
Owner string `json:"owner"`
PRNumber int `json:"pr_number"`
}{}
err := params.Unmarshal(&result)
if err != nil {
return types.ActionResult{}, err
}
if g.repository != "" && g.owner != "" {
result.Repository = g.repository
result.Owner = g.owner
}
pr, _, err := g.client.PullRequests.Get(ctx, result.Owner, result.Repository, result.PRNumber)
if err != nil {
return types.ActionResult{Result: fmt.Sprintf("Error fetching pull request: %s", err.Error())}, err
}
if pr == nil {
return types.ActionResult{Result: fmt.Sprintf("No pull request found")}, nil
}
// Get the list of changed files
files, _, err := g.client.PullRequests.ListFiles(ctx, result.Owner, result.Repository, result.PRNumber, &github.ListOptions{})
if err != nil {
return types.ActionResult{Result: fmt.Sprintf("Error fetching pull request files: %s", err.Error())}, err
}
// Build the file changes summary with patches
fileChanges := "\n\nFile Changes:\n"
for _, file := range files {
fileChanges += fmt.Sprintf("\n--- %s\n+++ %s\n", *file.Filename, *file.Filename)
if g.showFullDiff && file.Patch != nil {
fileChanges += *file.Patch
}
fileChanges += fmt.Sprintf("\n(%d additions, %d deletions)\n", *file.Additions, *file.Deletions)
}
return types.ActionResult{
Result: fmt.Sprintf(
"Pull Request %d Repository: %s\nTitle: %s\nBody: %s\nState: %s\nBase: %s\nHead: %s%s",
*pr.Number, *pr.Base.Repo.FullName, *pr.Title, *pr.Body, *pr.State, *pr.Base.Ref, *pr.Head.Ref, fileChanges)}, nil
}
func (g *GithubPRReader) Definition() types.ActionDefinition {
actionName := "read_github_pr"
if g.customActionName != "" {
actionName = g.customActionName
}
description := "Read a GitHub pull request."
if g.repository != "" && g.owner != "" {
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"pr_number": {
Type: jsonschema.Number,
Description: "The number of the pull request to read.",
},
},
Required: []string{"pr_number"},
}
}
return types.ActionDefinition{
Name: types.ActionDefinitionName(actionName),
Description: description,
Properties: map[string]jsonschema.Definition{
"pr_number": {
Type: jsonschema.Number,
Description: "The number of the pull request to read.",
},
"repository": {
Type: jsonschema.String,
Description: "The repository containing the pull request.",
},
"owner": {
Type: jsonschema.String,
Description: "The owner of the repository.",
},
},
Required: []string{"pr_number", "repository", "owner"},
}
}
func (a *GithubPRReader) Plannable() bool {
return true
}
// GithubPRReaderConfigMeta returns the metadata for GitHub PR Reader action configuration fields
func GithubPRReaderConfigMeta() []config.Field {
return []config.Field{
{
Name: "token",
Label: "GitHub Token",
Type: config.FieldTypeText,
Required: true,
HelpText: "GitHub API token with repository access",
},
{
Name: "repository",
Label: "Repository",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository name",
},
{
Name: "owner",
Label: "Owner",
Type: config.FieldTypeText,
Required: false,
HelpText: "GitHub repository owner",
},
{
Name: "customActionName",
Label: "Custom Action Name",
Type: config.FieldTypeText,
HelpText: "Custom name for this action",
},
{
Name: "showFullDiff",
Label: "Show Full Diff",
Type: config.FieldTypeCheckbox,
HelpText: "Whether to show the full diff content or just the summary",
},
}
}