From c529f880d3adbd0909fb5a086943b861ac9dc07c Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Fri, 2 May 2025 14:49:01 +0200 Subject: [PATCH] feat(github): add action to list and search files in a repository (#110) Signed-off-by: mudler --- services/actions.go | 18 ++ services/actions/githubrepositorylistfiles.go | 163 +++++++++++++++ .../actions/githubrepositorysearchfiles.go | 187 ++++++++++++++++++ 3 files changed, 368 insertions(+) create mode 100644 services/actions/githubrepositorylistfiles.go create mode 100644 services/actions/githubrepositorysearchfiles.go diff --git a/services/actions.go b/services/actions.go index d78b67b..c4ad928 100644 --- a/services/actions.go +++ b/services/actions.go @@ -35,6 +35,8 @@ const ( ActionGithubPRCreator = "github-pr-creator" ActionGithubGetAllContent = "github-get-all-repository-content" ActionGithubREADME = "github-readme" + ActionGithubRepositorySearchFiles = "github-repository-search-files" + ActionGithubRepositoryListFiles = "github-repository-list-files" ActionScraper = "scraper" ActionWikipedia = "wikipedia" ActionBrowse = "browse" @@ -56,6 +58,8 @@ var AvailableActions = []string{ ActionGithubIssueSearcher, ActionGithubRepositoryGet, ActionGithubGetAllContent, + ActionGithubRepositorySearchFiles, + ActionGithubRepositoryListFiles, ActionBrowserAgentRunner, ActionDeepResearchRunner, ActionGithubRepositoryCreateOrUpdate, @@ -145,6 +149,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP a = actions.NewGithubPRCreator(config) case ActionGithubGetAllContent: a = actions.NewGithubRepositoryGetAllContent(config) + case ActionGithubRepositorySearchFiles: + a = actions.NewGithubRepositorySearchFiles(config) + case ActionGithubRepositoryListFiles: + a = actions.NewGithubRepositoryListFiles(config) case ActionGithubIssueCommenter: a = actions.NewGithubIssueCommenter(config) case ActionGithubRepositoryGet: @@ -248,6 +256,16 @@ func ActionsConfigMeta() []config.FieldGroup { Label: "GitHub Get All Repository Content", Fields: actions.GithubRepositoryGetAllContentConfigMeta(), }, + { + Name: "github-repository-search-files", + Label: "GitHub Repository Search Files", + Fields: actions.GithubRepositorySearchFilesConfigMeta(), + }, + { + Name: "github-repository-list-files", + Label: "GitHub Repository List Files", + Fields: actions.GithubRepositoryListFilesConfigMeta(), + }, { Name: "github-repository-create-or-update-content", Label: "GitHub Repository Create/Update Content", diff --git a/services/actions/githubrepositorylistfiles.go b/services/actions/githubrepositorylistfiles.go new file mode 100644 index 0000000..65b23f6 --- /dev/null +++ b/services/actions/githubrepositorylistfiles.go @@ -0,0 +1,163 @@ +package actions + +import ( + "context" + "fmt" + "strings" + + "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 GithubRepositoryListFiles struct { + token, repository, owner, customActionName string + client *github.Client +} + +func NewGithubRepositoryListFiles(config map[string]string) *GithubRepositoryListFiles { + client := github.NewClient(nil).WithAuthToken(config["token"]) + + return &GithubRepositoryListFiles{ + client: client, + token: config["token"], + repository: config["repository"], + owner: config["owner"], + customActionName: config["customActionName"], + } +} + +func (g *GithubRepositoryListFiles) listFilesRecursively(ctx context.Context, path string, owner string, repository string) ([]string, error) { + var files []string + + // Get content at the current path + _, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil) + if err != nil { + return nil, fmt.Errorf("error getting content at path %s: %w", path, err) + } + + // Process each item in the directory + for _, item := range directoryContent { + if item.GetType() == "dir" { + // Recursively list files in subdirectories + subFiles, err := g.listFilesRecursively(ctx, item.GetPath(), owner, repository) + if err != nil { + return nil, err + } + files = append(files, subFiles...) + } else if item.GetType() == "file" { + // Add file path to the list + files = append(files, item.GetPath()) + } + } + + return files, nil +} + +func (g *GithubRepositoryListFiles) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { + result := struct { + Repository string `json:"repository"` + Owner string `json:"owner"` + Path string `json:"path,omitempty"` + }{} + 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 + } + + // Start from root if no path specified + if result.Path == "" { + result.Path = "." + } + + files, err := g.listFilesRecursively(ctx, result.Path, result.Owner, result.Repository) + if err != nil { + return types.ActionResult{}, err + } + + // Join all file paths with newlines for better readability + content := strings.Join(files, "\n") + return types.ActionResult{Result: content}, nil +} + +func (g *GithubRepositoryListFiles) Definition() types.ActionDefinition { + actionName := "list_github_repository_files" + if g.customActionName != "" { + actionName = g.customActionName + } + description := "List all files in a GitHub repository" + if g.repository != "" && g.owner != "" { + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "Optional path to start listing from (defaults to repository root)", + }, + }, + } + } + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "Optional path to start listing from (defaults to repository root)", + }, + "repository": { + Type: jsonschema.String, + Description: "The repository to list files from", + }, + "owner": { + Type: jsonschema.String, + Description: "The owner of the repository", + }, + }, + Required: []string{"repository", "owner"}, + } +} + +func (a *GithubRepositoryListFiles) Plannable() bool { + return true +} + +// GithubRepositoryListFilesConfigMeta returns the metadata for GitHub Repository List Files action configuration fields +func GithubRepositoryListFilesConfigMeta() []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", + }, + } +} diff --git a/services/actions/githubrepositorysearchfiles.go b/services/actions/githubrepositorysearchfiles.go new file mode 100644 index 0000000..576e975 --- /dev/null +++ b/services/actions/githubrepositorysearchfiles.go @@ -0,0 +1,187 @@ +package actions + +import ( + "context" + "fmt" + "strings" + + "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 GithubRepositorySearchFiles struct { + token, repository, owner, customActionName string + client *github.Client +} + +func NewGithubRepositorySearchFiles(config map[string]string) *GithubRepositorySearchFiles { + client := github.NewClient(nil).WithAuthToken(config["token"]) + + return &GithubRepositorySearchFiles{ + client: client, + token: config["token"], + repository: config["repository"], + owner: config["owner"], + customActionName: config["customActionName"], + } +} + +func (g *GithubRepositorySearchFiles) searchFilesRecursively(ctx context.Context, path string, owner string, repository string, searchPattern string) (string, error) { + var result strings.Builder + + // Get content at the current path + _, directoryContent, _, err := g.client.Repositories.GetContents(ctx, owner, repository, path, nil) + if err != nil { + return "", fmt.Errorf("error getting content at path %s: %w", path, err) + } + + // Process each item in the directory + for _, item := range directoryContent { + if item.GetType() == "dir" { + // Recursively search in subdirectories + subContent, err := g.searchFilesRecursively(ctx, item.GetPath(), owner, repository, searchPattern) + if err != nil { + return "", err + } + result.WriteString(subContent) + } else if item.GetType() == "file" { + // Check if file name matches the search pattern + if strings.Contains(strings.ToLower(item.GetName()), strings.ToLower(searchPattern)) { + // Get file content + fileContent, _, _, err := g.client.Repositories.GetContents(ctx, owner, repository, item.GetPath(), nil) + if err != nil { + return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err) + } + + content, err := fileContent.GetContent() + if err != nil { + return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err) + } + + // Add file content to result with clear markers + result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath())) + result.WriteString(content) + result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath())) + } + } + } + + return result.String(), nil +} + +func (g *GithubRepositorySearchFiles) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { + result := struct { + Repository string `json:"repository"` + Owner string `json:"owner"` + Path string `json:"path,omitempty"` + SearchPattern string `json:"searchPattern"` + }{} + 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 + } + + // Start from root if no path specified + if result.Path == "" { + result.Path = "." + } + + content, err := g.searchFilesRecursively(ctx, result.Path, result.Owner, result.Repository, result.SearchPattern) + if err != nil { + return types.ActionResult{}, err + } + + return types.ActionResult{Result: content}, nil +} + +func (g *GithubRepositorySearchFiles) Definition() types.ActionDefinition { + actionName := "search_github_repository_files" + if g.customActionName != "" { + actionName = g.customActionName + } + description := "Search for files in a GitHub repository and return their content" + if g.repository != "" && g.owner != "" { + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "Optional path to start searching from (defaults to repository root)", + }, + "searchPattern": { + Type: jsonschema.String, + Description: "Pattern to search for in file names (case-insensitive)", + }, + }, + Required: []string{"searchPattern"}, + } + } + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "Optional path to start searching from (defaults to repository root)", + }, + "repository": { + Type: jsonschema.String, + Description: "The repository to search in", + }, + "owner": { + Type: jsonschema.String, + Description: "The owner of the repository", + }, + "searchPattern": { + Type: jsonschema.String, + Description: "Pattern to search for in file names (case-insensitive)", + }, + }, + Required: []string{"repository", "owner", "searchPattern"}, + } +} + +func (a *GithubRepositorySearchFiles) Plannable() bool { + return true +} + +// GithubRepositorySearchFilesConfigMeta returns the metadata for GitHub Repository Search Files action configuration fields +func GithubRepositorySearchFilesConfigMeta() []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", + }, + } +}