Skip to content

Commit 3ced633

Browse files
Merge pull request #34 from bit8bytes/stores
feat: store #31
2 parents 862a117 + 132bf6d commit 3ced633

12 files changed

Lines changed: 321 additions & 95 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ go.work.sum
2525
# env file
2626
.env
2727

28+
/tmp

agents/agents.go

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,22 @@ import (
1010
"context"
1111
"errors"
1212
"fmt"
13-
"strings"
1413

1514
"github.com/bit8bytes/gogantic/agents/tools"
1615
"github.com/bit8bytes/gogantic/inputs/roles"
1716
"github.com/bit8bytes/gogantic/llms"
18-
"github.com/bit8bytes/gogantic/outputs/jsonout"
1917
)
2018

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

23+
type store interface {
24+
Add(ctx context.Context, msgs ...llms.Message) error
25+
List(ctx context.Context) ([]llms.Message, error)
26+
Clear(ctx context.Context) error
27+
}
28+
2529
// Tool represents an action the agent can perform.
2630
// Each tool must provide a name, description, and execution logic.
2731
type Tool interface {
@@ -41,40 +45,41 @@ type parser interface {
4145
type Agent struct {
4246
model llm
4347
tools map[string]Tool
44-
Messages []llms.Message
48+
History store
4549
actions []Action
4650
parser parser
4751
finalAnswer string
4852
}
4953

50-
// New creates an agent with the given model and tools.
51-
// The agent is initialized with a ReAct system prompt.
52-
func New(model llm, tools []Tool) *Agent {
53-
p := jsonout.NewParser[AgentResponse]()
54-
t := toolNames(tools)
55-
54+
// New creates an agent with the given model, tools, storage, and parser.
55+
// For the ReAct pattern, prefer NewReAct.
56+
func New(model llm, tools []Tool, storage store, p parser) *Agent {
5657
return &Agent{
57-
model: model,
58-
tools: t,
59-
Messages: buildReActPrompt(t, p.Instructions()),
60-
parser: p,
58+
model: model,
59+
tools: toolNames(tools),
60+
History: storage,
61+
parser: p,
6162
}
6263
}
6364

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

7474
// Plan calls the LLM to decide the next action or provide a final answer.
7575
// Returns Response.Finish=true when the task is complete.
7676
func (a *Agent) Plan(ctx context.Context) (*Response, error) {
77-
generated, err := a.model.Generate(ctx, a.Messages)
77+
history, err := a.History.List(ctx)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
generated, err := a.model.Generate(ctx, history)
7883
if err != nil {
7984
return nil, err
8085
}
@@ -84,7 +89,7 @@ func (a *Agent) Plan(ctx context.Context) (*Response, error) {
8489
return nil, fmt.Errorf("failed to parse agent response: %w", err)
8590
}
8691

87-
a.addAssistantMessage(generated.Result)
92+
a.addAssistantMessage(ctx, generated.Result)
8893

8994
if parsed.FinalAnswer != "" {
9095
a.finalAnswer = parsed.FinalAnswer
@@ -119,19 +124,19 @@ func (a *Agent) Act(ctx context.Context) {
119124
func (a *Agent) handleAction(ctx context.Context, action Action) bool {
120125
t, exists := a.tools[action.Tool]
121126
if !exists {
122-
a.addObservationMessage("The action " + action.Tool + " doesn't exist.")
127+
a.addObservationMessage(ctx, "The action "+action.Tool+" doesn't exist.")
123128
return false
124129
}
125130

126131
observation, err := t.Execute(ctx, tools.Input{
127132
Content: action.ToolInput,
128133
})
129134
if err != nil {
130-
a.addObservationMessage("Error: " + err.Error())
135+
a.addObservationMessage(ctx, "Error: "+err.Error())
131136
return false
132137
}
133138

134-
a.addObservationMessage(observation.Content)
139+
a.addObservationMessage(ctx, observation.Content)
135140
return true
136141
}
137142

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

151-
func buildReActPrompt(tools map[string]Tool, jsonInstructions string) []llms.Message {
152-
var toolDescriptions strings.Builder
153-
for _, t := range tools {
154-
fmt.Fprintf(&toolDescriptions, "- %s: %s\n", t.Name(), t.Description())
155-
}
156-
157-
return []llms.Message{
158-
{
159-
Role: roles.System,
160-
Content: fmt.Sprintf(`
161-
You are an helpful agent. Answer questions using the available tools.
162-
Do not estimate or predict values. Use only values returned by tools.
163-
164-
Available tools:
165-
%s
166-
%s
167-
168-
Respond with a JSON object on each turn with these fields:
169-
- "thought": your reasoning about what to do next
170-
- "action": the exact tool name to call (empty string when giving final answer)
171-
- "action_input": the input to pass to the tool (empty string when giving final answer)
172-
- "final_answer": your final answer (empty string when calling a tool)
173-
174-
Think step by step. Do not hallucinate.`, toolDescriptions.String(), jsonInstructions),
175-
},
176-
}
177-
}
178-
179156
func toolNames(tools []Tool) map[string]Tool {
180157
t := make(map[string]Tool, len(tools))
181158
for _, tool := range tools {

agents/messages.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package agents
22

33
import (
4+
"context"
5+
46
"github.com/bit8bytes/gogantic/inputs/roles"
57
"github.com/bit8bytes/gogantic/llms"
68
)
79

8-
func (a *Agent) addAssistantMessage(content string) {
9-
a.Messages = append(a.Messages, llms.Message{
10+
func (a *Agent) addAssistantMessage(ctx context.Context, content string) {
11+
a.History.Add(ctx, llms.Message{
1012
Role: roles.Assistent,
1113
Content: content,
1214
})
1315
}
1416

15-
func (a *Agent) addObservationMessage(observation string) {
16-
a.Messages = append(a.Messages, llms.Message{
17+
func (a *Agent) addObservationMessage(ctx context.Context, observation string) {
18+
a.History.Add(ctx, llms.Message{
1719
Role: roles.System,
1820
Content: "Observation: " + observation,
1921
})

agents/react.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package agents
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/bit8bytes/gogantic/inputs/roles"
9+
"github.com/bit8bytes/gogantic/llms"
10+
"github.com/bit8bytes/gogantic/outputs/jsonout"
11+
)
12+
13+
// NewReAct creates an agent pre-configured for the ReAct pattern.
14+
// It seeds the ReAct system prompt into storage.
15+
func NewReAct(ctx context.Context, model llm, tools []Tool, storage store) (*Agent, error) {
16+
p := jsonout.NewParser[AgentResponse]()
17+
t := toolNames(tools)
18+
19+
msgs := buildReActPrompt(t, p.Instructions())
20+
if err := storage.Add(ctx, msgs...); err != nil {
21+
return nil, err
22+
}
23+
24+
return &Agent{
25+
model: model,
26+
tools: t,
27+
History: storage,
28+
parser: p,
29+
}, nil
30+
}
31+
32+
func buildReActPrompt(tools map[string]Tool, jsonInstructions string) []llms.Message {
33+
var toolDescriptions strings.Builder
34+
for _, t := range tools {
35+
fmt.Fprintf(&toolDescriptions, "- %s: %s\n", t.Name(), t.Description())
36+
}
37+
38+
return []llms.Message{
39+
{
40+
Role: roles.System,
41+
Content: fmt.Sprintf(`
42+
You are an helpful agent. Answer questions using the available tools.
43+
Do not estimate or predict values. Use only values returned by tools.
44+
45+
Available tools:
46+
%s
47+
%s
48+
49+
Respond with a JSON object on each turn with these fields:
50+
- "thought": your reasoning about what to do next
51+
- "action": the exact tool name to call (empty string when giving final answer)
52+
- "action_input": the input to pass to the tool (empty string when giving final answer)
53+
- "final_answer": your final answer (empty string when calling a tool)
54+
55+
Think step by step. Do not hallucinate.`, toolDescriptions.String(), jsonInstructions),
56+
},
57+
}
58+
}

examples/agents/ollama/grep/main.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,61 @@ package main
22

33
import (
44
"context"
5+
"database/sql"
56
"fmt"
67
"time"
78

89
"github.com/bit8bytes/gogantic/agents"
910
"github.com/bit8bytes/gogantic/llms/ollama"
1011
"github.com/bit8bytes/gogantic/runner"
12+
"github.com/bit8bytes/gogantic/stores"
13+
modernc "github.com/bit8bytes/gogantic/stores/moderncsqlite"
14+
_ "github.com/bit8bytes/gogantic/stores/moderncsqlite"
1115
)
1216

1317
func main() {
14-
llm := ollama.New(ollama.Model{
18+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
19+
defer cancel()
20+
21+
db, err := sql.Open(modernc.Sqlite, ":memory:")
22+
if err != nil {
23+
panic(err)
24+
}
25+
defer db.Close()
26+
27+
_, err = db.ExecContext(ctx, `
28+
CREATE TABLE IF NOT EXISTS messages (
29+
id INTEGER PRIMARY KEY AUTOINCREMENT,
30+
role TEXT NOT NULL,
31+
content TEXT NOT NULL,
32+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
33+
);`)
34+
if err != nil {
35+
panic(err)
36+
}
37+
38+
storage, err := stores.New(ctx, modernc.Sqlite, db)
39+
if err != nil {
40+
panic(err)
41+
}
42+
defer storage.Close()
43+
44+
model := ollama.New(ollama.Model{
1545
Model: "gemma3n:e2b",
1646
Options: ollama.Options{NumCtx: 4096},
1747
Stream: false,
1848
Format: "json",
1949
})
2050

21-
tools := []agents.Tool{
22-
ListDir{},
51+
agent, err := agents.NewReAct(ctx, model, []agents.Tool{ListDir{}}, storage)
52+
if err != nil {
53+
panic(err)
2354
}
2455

25-
task := "List all files in folder agents/"
26-
agent := agents.New(llm, tools)
27-
if err := agent.Task(task); err != nil {
56+
if err := agent.Task(ctx, "List all files in folder agents/"); err != nil {
2857
panic(err)
2958
}
3059

31-
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*60)
32-
defer cancel()
33-
3460
r := runner.New(agent, true)
3561
if err := r.Run(ctx); err != nil {
3662
panic(err)

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
module github.com/bit8bytes/gogantic
22

33
go 1.25.7
4+
5+
require (
6+
github.com/dustin/go-humanize v1.0.1 // indirect
7+
github.com/google/uuid v1.6.0 // indirect
8+
github.com/mattn/go-isatty v0.0.20 // indirect
9+
github.com/ncruces/go-strftime v1.0.0 // indirect
10+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
11+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
12+
golang.org/x/sys v0.37.0 // indirect
13+
modernc.org/libc v1.67.6 // indirect
14+
modernc.org/mathutil v1.7.1 // indirect
15+
modernc.org/memory v1.11.0 // indirect
16+
modernc.org/sqlite v1.46.1 // indirect
17+
)

go.sum

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
3+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
7+
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
8+
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
9+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
10+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
11+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
12+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
13+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
15+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
16+
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
17+
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
18+
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
19+
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
20+
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
21+
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
22+
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
23+
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=

grep

-7.92 MB
Binary file not shown.

inputs/roles/roles.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ const (
77
User Role = "user"
88
Assistent Role = "assistent"
99
)
10+
11+
func (r Role) String() string {
12+
return string(r)
13+
}

0 commit comments

Comments
 (0)