Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ go.work.sum
# env file
.env

/tmp
75 changes: 26 additions & 49 deletions agents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ import (
"context"
"errors"
"fmt"
"strings"

"github.com/bit8bytes/gogantic/agents/tools"
"github.com/bit8bytes/gogantic/inputs/roles"
"github.com/bit8bytes/gogantic/llms"
"github.com/bit8bytes/gogantic/outputs/jsonout"
)

type llm interface {
Generate(ctx context.Context, messages []llms.Message) (*llms.ContentResponse, error)
}

type store interface {
Add(ctx context.Context, msgs ...llms.Message) error
List(ctx context.Context) ([]llms.Message, error)
Clear(ctx context.Context) error
}

// Tool represents an action the agent can perform.
// Each tool must provide a name, description, and execution logic.
type Tool interface {
Expand All @@ -41,40 +45,41 @@ type parser interface {
type Agent struct {
model llm
tools map[string]Tool
Messages []llms.Message
History store
actions []Action
parser parser
finalAnswer string
}

// New creates an agent with the given model and tools.
// The agent is initialized with a ReAct system prompt.
func New(model llm, tools []Tool) *Agent {
p := jsonout.NewParser[AgentResponse]()
t := toolNames(tools)

// New creates an agent with the given model, tools, storage, and parser.
// For the ReAct pattern, prefer NewReAct.
func New(model llm, tools []Tool, storage store, p parser) *Agent {
return &Agent{
model: model,
tools: t,
Messages: buildReActPrompt(t, p.Instructions()),
parser: p,
model: model,
tools: toolNames(tools),
History: storage,
parser: p,
}
}

// Task sets the user's question or task for the agent to solve.
// Call this before starting the Plan-Act loop.
func (a *Agent) Task(prompt string) error {
a.Messages = append(a.Messages, llms.Message{
func (a *Agent) Task(ctx context.Context, prompt string) error {
return a.History.Add(ctx, llms.Message{
Role: roles.User,
Content: "Question: " + prompt,
})
return nil
}

// Plan calls the LLM to decide the next action or provide a final answer.
// Returns Response.Finish=true when the task is complete.
func (a *Agent) Plan(ctx context.Context) (*Response, error) {
generated, err := a.model.Generate(ctx, a.Messages)
history, err := a.History.List(ctx)
if err != nil {
return nil, err
}

generated, err := a.model.Generate(ctx, history)
if err != nil {
return nil, err
}
Expand All @@ -84,7 +89,7 @@ func (a *Agent) Plan(ctx context.Context) (*Response, error) {
return nil, fmt.Errorf("failed to parse agent response: %w", err)
}

a.addAssistantMessage(generated.Result)
a.addAssistantMessage(ctx, generated.Result)

if parsed.FinalAnswer != "" {
a.finalAnswer = parsed.FinalAnswer
Expand Down Expand Up @@ -119,19 +124,19 @@ func (a *Agent) Act(ctx context.Context) {
func (a *Agent) handleAction(ctx context.Context, action Action) bool {
t, exists := a.tools[action.Tool]
if !exists {
a.addObservationMessage("The action " + action.Tool + " doesn't exist.")
a.addObservationMessage(ctx, "The action "+action.Tool+" doesn't exist.")
return false
}

observation, err := t.Execute(ctx, tools.Input{
Content: action.ToolInput,
})
if err != nil {
a.addObservationMessage("Error: " + err.Error())
a.addObservationMessage(ctx, "Error: "+err.Error())
return false
}

a.addObservationMessage(observation.Content)
a.addObservationMessage(ctx, observation.Content)
return true
}

Expand All @@ -148,34 +153,6 @@ func (a *Agent) Answer() (string, error) {
return a.finalAnswer, nil
}

func buildReActPrompt(tools map[string]Tool, jsonInstructions string) []llms.Message {
var toolDescriptions strings.Builder
for _, t := range tools {
fmt.Fprintf(&toolDescriptions, "- %s: %s\n", t.Name(), t.Description())
}

return []llms.Message{
{
Role: roles.System,
Content: fmt.Sprintf(`
You are an helpful agent. Answer questions using the available tools.
Do not estimate or predict values. Use only values returned by tools.

Available tools:
%s
%s

Respond with a JSON object on each turn with these fields:
- "thought": your reasoning about what to do next
- "action": the exact tool name to call (empty string when giving final answer)
- "action_input": the input to pass to the tool (empty string when giving final answer)
- "final_answer": your final answer (empty string when calling a tool)

Think step by step. Do not hallucinate.`, toolDescriptions.String(), jsonInstructions),
},
}
}

func toolNames(tools []Tool) map[string]Tool {
t := make(map[string]Tool, len(tools))
for _, tool := range tools {
Expand Down
10 changes: 6 additions & 4 deletions agents/messages.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package agents

import (
"context"

"github.com/bit8bytes/gogantic/inputs/roles"
"github.com/bit8bytes/gogantic/llms"
)

func (a *Agent) addAssistantMessage(content string) {
a.Messages = append(a.Messages, llms.Message{
func (a *Agent) addAssistantMessage(ctx context.Context, content string) {
a.History.Add(ctx, llms.Message{
Role: roles.Assistent,
Content: content,
})
}

func (a *Agent) addObservationMessage(observation string) {
a.Messages = append(a.Messages, llms.Message{
func (a *Agent) addObservationMessage(ctx context.Context, observation string) {
a.History.Add(ctx, llms.Message{
Role: roles.System,
Content: "Observation: " + observation,
})
Expand Down
58 changes: 58 additions & 0 deletions agents/react.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package agents

import (
"context"
"fmt"
"strings"

"github.com/bit8bytes/gogantic/inputs/roles"
"github.com/bit8bytes/gogantic/llms"
"github.com/bit8bytes/gogantic/outputs/jsonout"
)

// NewReAct creates an agent pre-configured for the ReAct pattern.
// It seeds the ReAct system prompt into storage.
func NewReAct(ctx context.Context, model llm, tools []Tool, storage store) (*Agent, error) {
p := jsonout.NewParser[AgentResponse]()
t := toolNames(tools)

msgs := buildReActPrompt(t, p.Instructions())
if err := storage.Add(ctx, msgs...); err != nil {
return nil, err
}

return &Agent{
model: model,
tools: t,
History: storage,
parser: p,
}, nil
}

func buildReActPrompt(tools map[string]Tool, jsonInstructions string) []llms.Message {
var toolDescriptions strings.Builder
for _, t := range tools {
fmt.Fprintf(&toolDescriptions, "- %s: %s\n", t.Name(), t.Description())
}

return []llms.Message{
{
Role: roles.System,
Content: fmt.Sprintf(`
You are an helpful agent. Answer questions using the available tools.
Do not estimate or predict values. Use only values returned by tools.

Available tools:
%s
%s

Respond with a JSON object on each turn with these fields:
- "thought": your reasoning about what to do next
- "action": the exact tool name to call (empty string when giving final answer)
- "action_input": the input to pass to the tool (empty string when giving final answer)
- "final_answer": your final answer (empty string when calling a tool)

Think step by step. Do not hallucinate.`, toolDescriptions.String(), jsonInstructions),
},
}
}
44 changes: 35 additions & 9 deletions examples/agents/ollama/grep/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,61 @@ package main

import (
"context"
"database/sql"
"fmt"
"time"

"github.com/bit8bytes/gogantic/agents"
"github.com/bit8bytes/gogantic/llms/ollama"
"github.com/bit8bytes/gogantic/runner"
"github.com/bit8bytes/gogantic/stores"
modernc "github.com/bit8bytes/gogantic/stores/moderncsqlite"
_ "github.com/bit8bytes/gogantic/stores/moderncsqlite"
)

func main() {
llm := ollama.New(ollama.Model{
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
defer cancel()

db, err := sql.Open(modernc.Sqlite, ":memory:")
if err != nil {
panic(err)
}
defer db.Close()

_, err = db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`)
if err != nil {
panic(err)
}

storage, err := stores.New(ctx, modernc.Sqlite, db)
if err != nil {
panic(err)
}
defer storage.Close()

model := ollama.New(ollama.Model{
Model: "gemma3n:e2b",
Options: ollama.Options{NumCtx: 4096},
Stream: false,
Format: "json",
})

tools := []agents.Tool{
ListDir{},
agent, err := agents.NewReAct(ctx, model, []agents.Tool{ListDir{}}, storage)
if err != nil {
panic(err)
}

task := "List all files in folder agents/"
agent := agents.New(llm, tools)
if err := agent.Task(task); err != nil {
if err := agent.Task(ctx, "List all files in folder agents/"); err != nil {
panic(err)
}

ctx, cancel := context.WithTimeout(context.TODO(), time.Second*60)
defer cancel()

r := runner.New(agent, true)
if err := r.Run(ctx); err != nil {
panic(err)
Expand Down
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
module github.com/bit8bytes/gogantic

go 1.25.7

require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/sys v0.37.0 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)
23 changes: 23 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
Binary file removed grep
Binary file not shown.
4 changes: 4 additions & 0 deletions inputs/roles/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ const (
User Role = "user"
Assistent Role = "assistent"
)

func (r Role) String() string {
return string(r)
}
Loading