Skip to content
Draft
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
5 changes: 5 additions & 0 deletions agents/analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package analyzer

type Analysis struct {
// TODO
}
9 changes: 6 additions & 3 deletions agents/documentor/documentor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package documentor
import (
"context"

"github.com/ATMackay/agent/state"
"github.com/ATMackay/agent/tools"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model"
)

const AgentName = "documentor"

type Documentor struct {
agent.Agent
}
Expand All @@ -22,7 +25,7 @@ func NewDocumentor(ctx context.Context, cfg *Config, model model.LLM) (*Document
functionTools, err := tools.GetTools([]tools.Kind{
tools.FetchRepoTree, // Fetch repository tree to understand the structure of the codebase.
tools.ReadFile, // Read specific files to understand code details and extract relevant information for documentation.
tools.SearchRepo, // Search the repository to find relevant code snippets or information.
tools.SearchFiles, // Search the repository to find relevant code snippets or information.
tools.WriteFile, // Write documentation or other output files.
}, &deps)
if err != nil {
Expand All @@ -31,12 +34,12 @@ func NewDocumentor(ctx context.Context, cfg *Config, model model.LLM) (*Document

// Instantiate Documentor LLM agent
da, err := llmagent.New(llmagent.Config{
Name: "documentor",
Name: AgentName,
Model: model,
Description: "Retrieves code from a GitHub repository and writes high-quality markdown documentation.",
Instruction: buildInstruction(),
Tools: functionTools,
OutputKey: tools.StateDocumentation,
OutputKey: state.StateDocumentation,
})
if err != nil {
return nil, err
Expand Down
15 changes: 15 additions & 0 deletions agents/documentor/prompt.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package documentor

import "google.golang.org/genai"

func buildInstruction() string {
return `
You are a code documentation agent.
Expand Down Expand Up @@ -69,3 +71,16 @@ Before each file read, ask: “What specific question am I trying to answer from
If that question is not specific, search first instead of reading.
`
}

// UserMessage returns the initial user message for the documentor service.
func UserMessage() *genai.Content {
return &genai.Content{
Role: "user",
Parts: []*genai.Part{
{
Text: "Generate detailed code documentation for the configured repository. " +
"Use fetch_repo_tree first, then read relevant files, then write the markdown output file.",
},
},
}
}
123 changes: 123 additions & 0 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"fmt"
"log/slog"
"os"

"github.com/ATMackay/agent/agents/documentor"
"github.com/ATMackay/agent/workflow"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/adk/session"
)

func NewAnalyzerCmd() *cobra.Command {
var repoURL string
var ref string
var pathPrefix string
var output string
var maxFiles int
var modelName, modelProvider string
var apiKey string

cmd := &cobra.Command{
Use: "analyzer",
Short: "Run the code analyzer agent",
RunE: func(cmd *cobra.Command, args []string) error {
// Prefer explicit flag, then env vars via Viper.
apiKey = viper.GetString("api-key")
if apiKey == "" {
return fmt.Errorf("google gemini or claude api key is required; set --api-key or export API_KEY")
}
if repoURL == "" {
return fmt.Errorf("--repo is required")
}
if output == "" {
return fmt.Errorf("--output is required")
}

workDir, err := os.MkdirTemp("", "agent-analyzer-*")
if err != nil {
return fmt.Errorf("create work dir: %w", err)
}
defer func() {
if err := os.RemoveAll(workDir); err != nil {
slog.Error("error removing body", "err", err)
}
}()

ctx := cmd.Context()

slog.Info(
"creating agent",
"agent_name", documentor.AgentName,
"dir", workDir,
"model", modelName,
"provider", modelProvider,
"output", output,
"repoURL", repoURL,
)

// Select model provider. Supported providers: 'claude' or gemini.
// modelCfg := &model.Config{
// Provider: model.Provider(modelProvider),
// Model: modelName,
// }
// mod, err := model.New(ctx, modelCfg.WithAPIKey(apiKey))
// if err != nil {
// return fmt.Errorf("create model: %w", err)
// }

// slog.Info(
// "created agent",
// "agent_name", ag.Name(),
// "agent_description", ag.Description(),
// )

initState := map[string]any{
// TODO
}

s, err := workflow.New(ctx, "analyzer", session.InMemoryService(), nil /* TODO */, initState)
if err != nil {
return fmt.Errorf("create runner: %w", err)
}

userMsg := documentor.UserMessage()

if err := s.Start(ctx, userCLI, userMsg); err != nil {
return err
}

if _, err := os.Stat(output); err != nil {
return fmt.Errorf("agent finished but output file was not created: %w", err)
}

slog.Info("Documentation written to", "output_file", output)
return nil
},
}

cmd.Flags().StringVar(&repoURL, "repo", "", "GitHub repository URL")
cmd.Flags().StringVar(&ref, "ref", "", "Optional branch, tag, or commit")
cmd.Flags().StringVar(&pathPrefix, "path", "", "Optional subdirectory to document")
cmd.Flags().StringVar(&output, "output", "doc.agentcli.md", "Output file path for the generated markdown")
cmd.Flags().IntVar(&maxFiles, "max-files", 50, "Maximum number of files to read")
cmd.Flags().StringVar(&modelName, "model", "claude-opus-4-1-20250805", "Language model to use")
cmd.Flags().StringVar(&modelProvider, "provider", "claude", "LLM provider to use (claude or gemini)")

// Bind flags to environment variables
must(viper.BindPFlag("repo", cmd.Flags().Lookup("repo")))
must(viper.BindPFlag("ref", cmd.Flags().Lookup("ref")))
must(viper.BindPFlag("path", cmd.Flags().Lookup("path")))
must(viper.BindPFlag("output", cmd.Flags().Lookup("output")))
must(viper.BindPFlag("max-files", cmd.Flags().Lookup("max-files")))
must(viper.BindPFlag("model", cmd.Flags().Lookup("model")))
must(viper.BindPFlag("provider", cmd.Flags().Lookup("provider")))

// API_KEY is preferred, GOOGLE_API_KEY, GEMINI_API_KEY, CLAUDE_API_KEY are accepted as fallback.
must(viper.BindEnv("api-key", "API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY", "CLAUDE_API_KEY"))

return cmd
}
18 changes: 15 additions & 3 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,21 @@ const EnvPrefix = "AGENT"

func NewAgentCLICmd() *cobra.Command {
cmd := &cobra.Command{
Use: "agent [subcommand]",
Short: fmt.Sprintf("agent command line interface.\n\nVERSION:\n semver: %s\n commit: %s\n compilation date: %s",
constants.Version, constants.GitCommit, constants.BuildDate),
Use: "agent [subcommand]",
Short: "CLI for running AI agents and workflows",
Long: fmt.Sprintf(`Agent CLI

Run and manage AI agents such as code documentors, reviewers, and other workflows.

Version:
semver: %s
commit: %s
build: %s
`,
constants.Version,
constants.GitCommit,
constants.BuildDate,
),
RunE: runHelp,
}

Expand Down
3 changes: 3 additions & 0 deletions cmd/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package cmd

const userCLI = "cli-user"
79 changes: 19 additions & 60 deletions cmd/documentor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@ import (
"fmt"
"log/slog"
"os"
"time"

"github.com/ATMackay/agent/agents/documentor"
"github.com/ATMackay/agent/model"
"github.com/ATMackay/agent/tools"
"github.com/ATMackay/agent/state"
"github.com/ATMackay/agent/workflow"
"github.com/spf13/cobra"
"github.com/spf13/viper"
agentpkg "google.golang.org/adk/agent"
"google.golang.org/adk/runner"
"google.golang.org/adk/session"
"google.golang.org/genai"
)

const userCLI = "cli-user"

func NewDocumentorCmd() *cobra.Command {
var repoURL string
var ref string
Expand Down Expand Up @@ -62,7 +57,8 @@ func NewDocumentorCmd() *cobra.Command {
ctx := cmd.Context()

slog.Info(
"creating documentor agent",
"creating agent",
"agent_name", documentor.AgentName,
"dir", workDir,
"model", modelName,
"provider", modelProvider,
Expand Down Expand Up @@ -91,68 +87,31 @@ func NewDocumentorCmd() *cobra.Command {
"agent_description", docAgent.Description(),
)

sessService := session.InMemoryService()
r, err := runner.New(runner.Config{
AppName: "documentor",
Agent: docAgent,
SessionService: sessService,
})
if err != nil {
return fmt.Errorf("create runner: %w", err)
}

initState := map[string]any{
tools.StateRepoURL: repoURL,
tools.StateRepoRef: ref,
tools.StateOutputPath: output,
tools.StateMaxFiles: maxFiles,
state.StateRepoURL: repoURL,
state.StateRepoRef: ref,
state.StateOutputPath: output,
state.StateMaxFiles: maxFiles,
}
if pathPrefix != "" {
initState[tools.StateSubPath] = pathPrefix
initState[state.StateSubPath] = pathPrefix
}

resp, err := sessService.Create(ctx, &session.CreateRequest{
AppName: "documentor",
UserID: userCLI,
State: initState,
})
s, err := workflow.New(
ctx,
documentor.AgentName,
session.InMemoryService(),
docAgent,
initState)
if err != nil {
return fmt.Errorf("create session: %w", err)
}

userMsg := &genai.Content{
Role: "user",
Parts: []*genai.Part{
{
Text: "Generate detailed code documentation for the configured repository. " +
"Use fetch_repo_tree first, then read relevant files, then write the markdown output file.",
},
},
return err
}

slog.Info(
"running documentor agent",
"session_id", resp.Session.ID(),
)
userMsg := documentor.UserMessage()

start := time.Now()
for event, err := range r.Run(ctx, userCLI, resp.Session.ID(), userMsg, agentpkg.RunConfig{}) {
if err != nil {
return fmt.Errorf("agent error: %w", err)
}
// handle event (log)
if event.UsageMetadata == nil {
continue
}
slog.Info("event", "author", event.Author, "event_id", event.ID, "prompt_tokens", event.UsageMetadata.PromptTokenCount, "total_tokens", event.UsageMetadata.TotalTokenCount)
if event.Content == nil {
continue
}
for _, p := range event.Content.Parts {
slog.Debug("response_content", "role", event.Content.Role, "text", p.Text, "function_call", p.FunctionCall)
}
if err := s.Start(ctx, userCLI, userMsg); err != nil {
return err
}
slog.Info("Agent execution complete", "time_taken", time.Since(start))

if _, err := os.Stat(output); err != nil {
return fmt.Errorf("agent finished but output file was not created: %w", err)
Expand Down
1 change: 1 addition & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func NewRunCmd() *cobra.Command {

// Add subcommands
cmd.AddCommand(NewDocumentorCmd())
cmd.AddCommand(NewAnalyzerCmd())
// TODO - more agent types

// Bind flags and ENV vars
Expand Down
2 changes: 1 addition & 1 deletion tools/state.go → state/state.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tools
package state

// Session state keys. TODO agent specific, might refactor...
const (
Expand Down
12 changes: 12 additions & 0 deletions tools/edit_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tools

type EditFileArgs struct {
Path string `json:"path"`
StartLine string `json:"start_line"`
EndLine int `json:"end_line,omitempty"`
Content string `json:"content"` // multi-line content separated by '\n'
}

type EditFileResult struct {
// TODO
}
Loading
Loading