Skip to content

Commit 3cca831

Browse files
committed
feat: implement go code analysis tools #41
1 parent 2ff21c4 commit 3cca831

21 files changed

Lines changed: 578 additions & 541 deletions

agents/agents.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616
"github.com/bit8bytes/gogantic/tools"
1717
)
1818

19+
var (
20+
ErrNoFinalAnswer = errors.New("no final answer available")
21+
)
22+
1923
type llm interface {
2024
Generate(ctx context.Context, messages []llms.Message) (*llms.ContentResponse, error)
2125
}
@@ -89,27 +93,32 @@ func (a *Agent) Plan(ctx context.Context) (*Response, error) {
8993
return nil, fmt.Errorf("failed to parse agent response: %w", err)
9094
}
9195

96+
if parsed.FinalAnswer == "" && parsed.Action == "" {
97+
if err := a.addAssistantMessage(ctx, generated.Result); err != nil {
98+
return nil, fmt.Errorf("failed to store assistant message: %w", err)
99+
}
100+
if err := a.addObservationMessage(ctx, "Your last response was incomplete: it contained neither an action nor a final answer. Please continue: either call a tool or provide your final answer."); err != nil {
101+
return nil, err
102+
}
103+
return &Response{Thought: parsed.Thought}, nil
104+
}
105+
92106
if err := a.addAssistantMessage(ctx, generated.Result); err != nil {
93107
return nil, fmt.Errorf("failed to store assistant message: %w", err)
94108
}
95109

96110
if parsed.FinalAnswer != "" {
97111
a.finalAnswer = parsed.FinalAnswer
98-
return &Response{Finish: true}, nil
99-
}
100-
101-
if parsed.Action == "" {
102-
return nil, errors.New("agent response contains neither a final answer nor an action")
112+
return &Response{Thought: parsed.Thought, Finish: true}, nil
103113
}
104114

105-
a.actions = []Action{
106-
{
107-
Tool: parsed.Action,
108-
ToolInput: parsed.ActionInput,
109-
},
115+
action := Action{
116+
Tool: parsed.Action,
117+
ToolInput: parsed.ActionInput,
110118
}
119+
a.actions = []Action{action}
111120

112-
return &Response{}, nil
121+
return &Response{Thought: parsed.Thought, Actions: []Action{action}}, nil
113122
}
114123

115124
// Act executes the tool chosen by Plan and adds the result as an observation.
@@ -148,7 +157,7 @@ func (a *Agent) clearActions() {
148157
// Only call this after Plan returns Finish=true.
149158
func (a *Agent) Answer() (string, error) {
150159
if a.finalAnswer == "" {
151-
return "", errors.New("no final answer available")
160+
return "", ErrNoFinalAnswer
152161
}
153162
return a.finalAnswer, nil
154163
}

agents/react.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agents
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strings"
78

89
"github.com/bit8bytes/gogantic/inputs/roles"
@@ -35,13 +36,17 @@ func buildReActPrompt(tools map[string]Tool, jsonInstructions string) []llms.Mes
3536
fmt.Fprintf(&toolDescriptions, "- %s: %s\n", t.Name(), t.Description())
3637
}
3738

39+
wd, _ := os.Getwd()
40+
3841
return []llms.Message{
3942
{
4043
Role: roles.System,
4144
Content: fmt.Sprintf(`
4245
You are an helpful agent. Answer questions using the available tools.
4346
Do not estimate or predict values. Use only values returned by tools.
4447
48+
Working directory: %s
49+
4550
Available tools:
4651
%s
4752
%s
@@ -54,7 +59,7 @@ Respond with a JSON object on each turn with these fields:
5459
5560
When you have enough information to answer, set "action" and "action_input" to "" and put a detailed answer based on your observations — MUST be non-empty when done; be thorough and include all relevant findings.
5661
57-
Think step by step. Do not hallucinate.`, toolDescriptions.String(), jsonInstructions),
62+
Think step by step. Do not hallucinate.`, wd, toolDescriptions.String(), jsonInstructions),
5863
},
5964
}
6065
}

agents/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ type Action struct {
1717
// Response indicates whether the agent loop should continue or finish.
1818
type Response struct {
1919
Actions []Action
20+
Thought string
2021
Finish bool
2122
}
Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"database/sql"
6+
"errors"
67
"fmt"
78
"time"
89

@@ -59,24 +60,20 @@ func main() {
5960

6061
// These tools are specifically designed for Golang.
6162
tools := []agents.Tool{
62-
tools.ListGoPackages{},
63-
tools.ParseGoFile{},
64-
tools.FindIdent{},
63+
tools.ListDeclarations{},
6564
tools.FindCalls{},
66-
tools.FindImporters{},
67-
tools.FindImplementors{},
65+
tools.FindUsages{},
66+
tools.GetFunctionSignature{},
67+
tools.GetStructFields{},
68+
tools.RunGoVet{},
6869
}
6970

7071
agent, err := agents.NewReAct(ctx, model, tools, storage)
7172
if err != nil {
7273
panic(err)
7374
}
7475

75-
task := `
76-
Analyze the current Go code base and evaluate its maintainability. Produce a short report.`
77-
// task := `
78-
// Find all types in this codebase that implement the parser interface.
79-
// For each type, show which package and file it is defined in.`
76+
task := `Where is the NewReAct function called?`
8077
if err := agent.Task(ctx, task); err != nil {
8178
panic(err)
8279
}
@@ -87,8 +84,9 @@ func main() {
8784
}
8885

8986
finalAnswer, err := agent.Answer()
90-
if err != nil {
91-
panic(err)
87+
if errors.Is(err, agents.ErrNoFinalAnswer) {
88+
fmt.Println("No final answer found")
89+
return
9290
}
9391
fmt.Println(finalAnswer)
9492
}

examples/outputs/jsonout/main.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

runner/runner.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ RUN:
4747
return fmt.Errorf("planning failed: %w", err)
4848
}
4949

50+
if r.printMessages {
51+
if response.Thought != "" {
52+
fmt.Printf("%sTHOUGHT:%s %s\n", cyan, reset, response.Thought)
53+
}
54+
for _, a := range response.Actions {
55+
fmt.Printf("%sACTION:%s %s(%s)\n", blue, reset, a.Tool, a.ToolInput)
56+
}
57+
}
58+
5059
if response.Finish {
5160
return nil
5261
}

tools/ast.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// NOTE: This file was generated by AI and needs to be evaluated and refactored before production use.
21
package tools
32

43
import (
@@ -10,6 +9,47 @@ import (
109
"strings"
1110
)
1211

12+
func docString(cg *ast.CommentGroup) string {
13+
if cg == nil {
14+
return ""
15+
}
16+
return strings.TrimSpace(cg.Text())
17+
}
18+
19+
func exprString(e ast.Expr) string {
20+
switch t := e.(type) {
21+
case *ast.Ident:
22+
return t.Name
23+
case *ast.StarExpr:
24+
return "*" + exprString(t.X)
25+
case *ast.SelectorExpr:
26+
return exprString(t.X) + "." + t.Sel.Name
27+
case *ast.ArrayType:
28+
return "[]" + exprString(t.Elt)
29+
case *ast.MapType:
30+
return "map[" + exprString(t.Key) + "]" + exprString(t.Value)
31+
case *ast.InterfaceType:
32+
return "interface{}"
33+
case *ast.StructType:
34+
return "struct{}"
35+
case *ast.Ellipsis:
36+
return "..." + exprString(t.Elt)
37+
default:
38+
return "..."
39+
}
40+
}
41+
42+
func recvTypeName(e ast.Expr) string {
43+
switch t := e.(type) {
44+
case *ast.Ident:
45+
return t.Name
46+
case *ast.StarExpr:
47+
return recvTypeName(t.X)
48+
default:
49+
return ""
50+
}
51+
}
52+
1353
func walkGoFiles(dir string, fset *token.FileSet) ([]*ast.File, error) {
1454
var files []*ast.File
1555
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {

tools/findCalls.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
// NOTE: This file was generated by AI and needs to be evaluated and refactored before production use.
21
package tools
32

43
import (
54
"context"
65
"fmt"
76
"go/ast"
87
"go/token"
8+
"path/filepath"
99
"strings"
1010
)
1111

@@ -16,7 +16,8 @@ func (t FindCalls) Name() string { return "FindCalls" }
1616
func (t FindCalls) Description() string {
1717
return `Find all call sites of a function by name in a Go codebase.
1818
Matches both direct calls (f()) and method calls (obj.f()).
19-
Input: two lines — directory path, then function name (e.g. 'Println').`
19+
Input: two lines — absolute filesystem path to a directory containing .go files, then function name.
20+
Use the working directory from the system prompt as the starting path.`
2021
}
2122

2223
func (t FindCalls) Execute(ctx context.Context, input Input) (Output, error) {
@@ -41,7 +42,8 @@ func (t FindCalls) Execute(ctx context.Context, input Input) (Output, error) {
4142
}
4243
if callFuncName(call.Fun) == funcName {
4344
pos := fset.Position(call.Pos())
44-
fmt.Fprintf(&b, "%s:%d\n", pos.Filename, pos.Line)
45+
rel, _ := filepath.Rel(dir, pos.Filename)
46+
fmt.Fprintf(&b, "%s:%d\n", rel, pos.Line)
4547
found++
4648
}
4749
return true

tools/findIdent.go

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)