From e911c4d9b095a97ce15638c3e032119cc883933a Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Fri, 11 Jul 2025 19:08:33 -0400 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20bring=20back=20the=20agent=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +- agent/agent.go | 242 ++++++++++++++++++ agent/messages.go | 38 +++ agent/types.go | 22 ++ .../ollama/filesystem/altered_foobar.txt | 1 + examples/agent/ollama/filesystem/foobar.txt | 1 + examples/agent/ollama/filesystem/main.go | 108 ++++++++ examples/agent/ollama/temperature/main.go | 71 +++++ examples/agent/ollama/time/main.go | 48 ++++ examples/agent/openai/temperature/main.go | 82 ++++++ runner/runner.go | 88 +++++++ tool/tool.go | 19 ++ 12 files changed, 732 insertions(+), 1 deletion(-) create mode 100644 agent/agent.go create mode 100644 agent/messages.go create mode 100644 agent/types.go create mode 100644 examples/agent/ollama/filesystem/altered_foobar.txt create mode 100644 examples/agent/ollama/filesystem/foobar.txt create mode 100644 examples/agent/ollama/filesystem/main.go create mode 100644 examples/agent/ollama/temperature/main.go create mode 100644 examples/agent/ollama/time/main.go create mode 100644 examples/agent/openai/temperature/main.go create mode 100644 runner/runner.go create mode 100644 tool/tool.go diff --git a/README.md b/README.md index a4f059d..8a254c2 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,18 @@ fmt.Println("Translate from", result.InputLanguage, " to ", result.OutputLanguag fmt.Println("Result: ", result.Text) ``` -Go to [Examples](/docs/EXAMPLES.md) for more info. +### MCP Tool Calling + +Gogantic supports MCP (Model Context Protocol) for tool calling: + +```go +// Create MCP-enabled pipe with tool calling +mcpPipe := pipe.NewMCP(messages, llm, parser, mcpClient) +result, _ := mcpPipe.InvokeWithTools(context.Background()) +fmt.Println("Answer:", result.Answer) +``` + +Go to [Examples](/docs/EXAMPLES.md) and [MCP Documentation](/docs/MCP.md) for more info. ## 📚 Sources and Inspiration diff --git a/agent/agent.go b/agent/agent.go new file mode 100644 index 0000000..6f42a39 --- /dev/null +++ b/agent/agent.go @@ -0,0 +1,242 @@ +package agent + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/bit8bytes/gogantic/input/chat" + "github.com/bit8bytes/gogantic/llm" + "github.com/bit8bytes/gogantic/tool" +) + +type Agent struct { + Model llm.LLM + Tools map[string]tool.Tool + Messages []llm.Message + Actions []Action +} + +func getToolNames(tools map[string]tool.Tool) string { + names := make([]string, 0, len(tools)) + for _, tool := range tools { + names = append(names, tool.Name()) + names = append(names, tool.Schema()) + } + return strings.Join(names, ", ") +} + +func New(model llm.LLM, tools map[string]tool.Tool) *Agent { + toolNames := getToolNames(tools) + initialMessages := setupReActPromptInitialMessages(toolNames) + + return &Agent{ + Model: model, + Tools: tools, + Messages: initialMessages, + } +} + +// Task the agent is going to execute +func (a *Agent) Task(prompt string) { + chatPrompt, _ := chat.New([]llm.Message{{Role: "user", Content: "Question: {{.input}}\n"}}) + + data := map[string]any{"input": prompt} + + formattedMessages, err := chatPrompt.Format(data) + if err != nil { + panic(err) + } + + a.Messages = append(a.Messages, formattedMessages...) +} + +// Identifies the generated messages and splits them into thought, action and action input +func (a *Agent) Plan(ctx context.Context) (*Response, error) { + generatedContent, err := a.Model.GenerateContent(ctx, a.Messages) + if err != nil { + return nil, err + } + + text := generatedContent.Result + + final := extractAfterLabel(text, "FINAL ANSWER:") + if len(final) > 0 { + a.Messages = append(a.Messages, llm.Message{ + Role: "assistant", + Content: fmt.Sprintf("\nFinal Answer: %s", final), + }) + + return &Response{Finish: true}, nil + } + + thought := extractAfterLabel(text, "Thought: ") + + // "Action: [ToolName]" + action := extractAfterLabel(text, "Action: ") + + // "Action Input: "input" + actionInput := extractAfterLabel(text, "Action Input: ") + + if len(thought) > 1 { + a.addThoughtMessage(strings.TrimSpace(thought)) + } + + if len(action) > 1 { + tool := extractSquareBracketsContent(action) + a.addActionMessage(tool) + + inputText := "" + if len(actionInput) > 1 { + inputText = removeQuotes(actionInput) + a.addActionInputMessage("\"" + inputText + "\"") + } else { + a.addActionInputMessage("\"\"") + } + + a.Actions = []Action{ + { + Tool: tool, + ToolInput: inputText, + }, + } + } else { + fmt.Println("Warning: No action found in response") + } + + return &Response{}, nil +} + +// Uses the given tools to get observations +func (a *Agent) Act(ctx context.Context) { + for _, action := range a.Actions { + if !a.handleAction(ctx, action) { + return + } + } + a.clearActions() +} + +// Handle action is a helper function that calls the tool selected by the LLM and adds the observation output +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.") + return false + } + + i := tool.Input{ + Content: action.ToolInput, + } + + observation, err := t.Call(ctx, i) + if err != nil { + a.addObservationMessage("Error: " + err.Error()) + return false + } + + a.addObservationMessage(observation.Content) + return true +} + +func (a *Agent) clearActions() { + a.Actions = nil +} + +func (a *Agent) GetFinalAnswer() (string, error) { + if len(a.Messages) == 0 { + return "", errors.New("No messages provided") + } + finalAnswer := a.Messages[len(a.Messages)-1].Content + parts := strings.Split(finalAnswer, "Final Answer: ") + if len(parts) < 2 { + return "", errors.New("Invalid final answer") + } + return parts[1], nil +} + +func setupReActPromptInitialMessages(tools string) []llm.Message { + reActPrompt, _ := chat.New([]llm.Message{ + {Role: "user", Content: ` +Answer the following questions as best you can. +Use only values from the tools. Do not estimate or predict values. +Select the tool that fits the question: + +[{{.tools}}] + +Use the following format: + +Thought: you should always think about what to do +Action: [Toolname] the action (only one at a time) to take in suqare braces e.g [NameOfTool] +Action Input: "input" the input value for the action in quotes e.g. "value" from Schema +Observation: the result of the action +... (this Thought: .../Action: [Toolname]/Action Input: "input"/Observation: ... can repeat N times) +Thought: I now know the final answer +FINAL ANSWER: the final answer to the original input question + +Think in steps. Don't hallucinate. Don't make up answers. +`}, + }) + + data := map[string]interface{}{ + "tools": tools, + } + + formattedMessages, err := reActPrompt.Format(data) + if err != nil { + panic(err) + } + + return formattedMessages +} + +func extractAfterLabel(s, label string) string { + startIndex := strings.Index(s, label) + if startIndex == -1 { + return "" // Label not found + } + startIndex += len(label) + for startIndex < len(s) && s[startIndex] == ' ' { + startIndex++ + } + endIndex := strings.Index(s[startIndex:], "\n") + if endIndex == -1 { + endIndex = len(s) + } else { + endIndex += startIndex + } + + return s[startIndex:endIndex] +} + +func removeSquareBrackets(s string) string { + s = strings.TrimSpace(s) + if len(s) >= 2 && s[0] == '[' && s[len(s)-1] == ']' { + return s[1 : len(s)-1] + } + return s +} + +func extractSquareBracketsContent(s string) string { + startIndex := strings.Index(s, "[") + if startIndex == -1 { + return "" // No opening bracket found + } + + endIndex := strings.Index(s[startIndex:], "]") + if endIndex == -1 { + return "" // No closing bracket found + } + + // Extract the content between brackets + return s[startIndex+1 : startIndex+endIndex] +} + +func removeQuotes(s string) string { + s = strings.TrimSpace(s) + if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { + return s[1 : len(s)-1] + } + return s +} diff --git a/agent/messages.go b/agent/messages.go new file mode 100644 index 0000000..3c1a0ea --- /dev/null +++ b/agent/messages.go @@ -0,0 +1,38 @@ +package agent + +import ( + "fmt" + + "github.com/bit8bytes/gogantic/llm" +) + +func (a *Agent) addObservationMessage(observation string) { + a.Messages = append(a.Messages, llm.Message{ + Role: "system", // Use system role for observations + Content: "Observation: " + observation, + }) +} + +// Helper method to add thought message +func (a *Agent) addThoughtMessage(thought string) { + a.Messages = append(a.Messages, llm.Message{ + Role: "assistant", + Content: "Thought: " + thought, + }) +} + +// Helper method to add action message +func (a *Agent) addActionMessage(action string) { + a.Messages = append(a.Messages, llm.Message{ + Role: "assistant", + Content: fmt.Sprintf(`Action: [%s]`, action), + }) +} + +// Helper method to add action input message +func (a *Agent) addActionInputMessage(input string) { + a.Messages = append(a.Messages, llm.Message{ + Role: "assistant", + Content: `Action Input: ` + input, + }) +} diff --git a/agent/types.go b/agent/types.go new file mode 100644 index 0000000..70004a7 --- /dev/null +++ b/agent/types.go @@ -0,0 +1,22 @@ +package agent + +type Step struct { + Thought string + Actions string + Observation string +} + +type Action struct { + Tool string + ToolInput string + ToolID string +} + +type Finish struct { + ReturnValues map[string]any +} + +type Response struct { + Actions []Action + Finish bool +} diff --git a/examples/agent/ollama/filesystem/altered_foobar.txt b/examples/agent/ollama/filesystem/altered_foobar.txt new file mode 100644 index 0000000..12a6c5f --- /dev/null +++ b/examples/agent/ollama/filesystem/altered_foobar.txt @@ -0,0 +1 @@ +Hello Foo!I can edit files. I am a happy local Agent! \ No newline at end of file diff --git a/examples/agent/ollama/filesystem/foobar.txt b/examples/agent/ollama/filesystem/foobar.txt new file mode 100644 index 0000000..23332fb --- /dev/null +++ b/examples/agent/ollama/filesystem/foobar.txt @@ -0,0 +1 @@ +Hello Foo! \ No newline at end of file diff --git a/examples/agent/ollama/filesystem/main.go b/examples/agent/ollama/filesystem/main.go new file mode 100644 index 0000000..a7cb6b7 --- /dev/null +++ b/examples/agent/ollama/filesystem/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/bit8bytes/gogantic/agent" + "github.com/bit8bytes/gogantic/llm/ollama" + "github.com/bit8bytes/gogantic/runner" + "github.com/bit8bytes/gogantic/tool" +) + +// FileParams defines the structure for SaveToFile parameters +type FileParams struct { + Content string `json:"content"` + Filename string `json:"filename"` +} + +type OpenFile struct{} +type WriteAndSaveToFile struct{} + +func main() { + model := ollama.Model{ + Model: "gemma3:4b", + Options: ollama.Options{NumCtx: 4096}, + Stream: false, + Stop: []string{"\nObservation", "Observation"}, + } + + llm := ollama.New(model) + tools := map[string]tool.Tool{ + "OpenFile": OpenFile{}, + "WriteAndSaveToFile": WriteAndSaveToFile{}, + } + + agent := agent.New(llm, tools) + agent.Task(` +1. Open the file foobar.txt. +2. Read the content and add the sentence: I can edit files. I am a happy local Agent! +3. Save it to altered_foobar.txt +`) + runner := runner.New(agent, runner.WithShowMessages()) + + runner.Run(context.TODO()) + finalAnswer1, _ := agent.GetFinalAnswer() + fmt.Println("Agent 1 final answer:", finalAnswer1) +} + +func (c OpenFile) Name() string { return "OpenFile" } + +func (c OpenFile) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + if input.Content == "" { + return tool.Output{Content: ""}, errors.New(`please provide the filename in the Action Input: "filename"`) + } + + content, err := os.ReadFile(input.Content) + if err != nil { + return tool.Output{Content: ""}, errors.New(err.Error()) + } + + response := "The content of the file is " + string(content) + + return tool.Output{Content: response}, nil +} + +func (c WriteAndSaveToFile) Name() string { return "WriteAndSaveToFile" } + +func (c WriteAndSaveToFile) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + fmt.Println(input) + + cleanedInput := strings.ReplaceAll(input.Content, `\"`, `"`) + cleanedInput = strings.ReplaceAll(cleanedInput, `'`, ``) + cleanedInput = strings.Trim(cleanedInput, `"`) + + var params FileParams + if err := json.Unmarshal([]byte(cleanedInput), ¶ms); err != nil { + fmt.Println("JSON parsing error:", err) + + if err := json.Unmarshal([]byte(input.Content), ¶ms); err != nil { + return tool.Output{}, errors.New(`please provide a valid JSON with "content" and "filename" fields for tool "WriteAndSaveToFile"`) + } + } + + if params.Filename == "" { + return tool.Output{}, errors.New(`please provide the filename in the "filename" field`) + } + + if params.Content == "" { + return tool.Output{}, errors.New(`please provide content to write in the "content" field`) + } + + file, err := os.Create(params.Filename) + if err != nil { + return tool.Output{}, err + } + defer file.Close() + + _, err = file.WriteString(params.Content) + if err != nil { + return tool.Output{}, err + } + + return tool.Output{Content: fmt.Sprintf("Successfully wrote to %s", params.Filename)}, nil +} diff --git a/examples/agent/ollama/temperature/main.go b/examples/agent/ollama/temperature/main.go new file mode 100644 index 0000000..ba2bead --- /dev/null +++ b/examples/agent/ollama/temperature/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "fmt" + "strconv" + + "github.com/bit8bytes/gogantic/agent" + "github.com/bit8bytes/gogantic/llm/ollama" + "github.com/bit8bytes/gogantic/runner" + "github.com/bit8bytes/gogantic/tool" +) + +const ( + ToolGetTemperatureInFahrenheit = "get_temperature_in_fahrenheit" + ToolFormatFahrenheitToCelsius = "format_fahrenheit_to_celsius" +) + +type GetTemperatureInFahrenheit struct{} +type FormatFahrenheitToCelsius struct{} + +func main() { + model := ollama.Model{ + Model: "gemma3n:e2b", + Options: ollama.Options{NumCtx: 4096}, + Stream: false, + Stop: []string{"\nObservation", "Observation"}, // Necessary due to the ReAct Prompt Pattern + } + llm := ollama.New(model) + + tools := map[string]tool.Tool{ + ToolGetTemperatureInFahrenheit: GetTemperatureInFahrenheit{}, + ToolFormatFahrenheitToCelsius: FormatFahrenheitToCelsius{}, + } + + weatherAgent := agent.New(llm, tools) + weatherAgent.Task("What is the current temperature and what is the current temperature in Celsius?") + + runner := runner.New(weatherAgent, + runner.WithIterationLimit(10), + runner.WithShowMessages()) + runner.Run(context.TODO()) + + finalAnswer, _ := weatherAgent.GetFinalAnswer() + fmt.Println(finalAnswer) +} + +func (t GetTemperatureInFahrenheit) Name() string { + return ToolGetTemperatureInFahrenheit +} + +func (t GetTemperatureInFahrenheit) Schema() string { return `()` } + +func (t GetTemperatureInFahrenheit) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + // This is only for showcase. + // If you want to use this and handle input e.g. location look at the math agent example. + return tool.Output{Content: "15.54°F"}, nil +} + +func (t FormatFahrenheitToCelsius) Name() string { + return ToolFormatFahrenheitToCelsius +} + +func (t FormatFahrenheitToCelsius) Schema() string { return `(0000)` } + +func (t FormatFahrenheitToCelsius) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + // Still, I do not handle errors in here. This has to be done through testing. + fahrenheit, _ := strconv.ParseFloat(input.Content, 64) + celsius := (fahrenheit - 32) * (5.0 / 9.0) + return tool.Output{Content: fmt.Sprintf("Current temperature: %.2f°C", celsius)}, nil +} diff --git a/examples/agent/ollama/time/main.go b/examples/agent/ollama/time/main.go new file mode 100644 index 0000000..ca058e6 --- /dev/null +++ b/examples/agent/ollama/time/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/bit8bytes/gogantic/agent" + "github.com/bit8bytes/gogantic/llm/ollama" + "github.com/bit8bytes/gogantic/runner" + "github.com/bit8bytes/gogantic/tool" +) + +type GetTime struct{} + +func main() { + model := ollama.Model{ + Model: "gemma3n:e2b", + Options: ollama.Options{NumCtx: 4096}, + Stream: false, + Stop: []string{"\nObservation", "Observation"}, + } + llm := ollama.New(model) + + tools := map[string]tool.Tool{ + "GetTime": GetTime{}, + } + + timeAgent := agent.New(llm, tools) + timeAgent.Task("What time is it?") + + ctx := context.TODO() + runner := runner.New(timeAgent, runner.WithShowMessages()) + runner.Run(ctx) + + finalAnswer, _ := timeAgent.GetFinalAnswer() + fmt.Println(finalAnswer) +} + +func (t GetTime) Name() string { return "GetTime" } + +func (t GetTime) Schema() string { return `()` } + +func (t GetTime) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + currentTime := time.Now() + fmtCurrentTime := currentTime.Format("2006-01-02 3:04:05 PM") + return tool.Output{Content: fmtCurrentTime}, nil +} diff --git a/examples/agent/openai/temperature/main.go b/examples/agent/openai/temperature/main.go new file mode 100644 index 0000000..754a7bf --- /dev/null +++ b/examples/agent/openai/temperature/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + + "github.com/bit8bytes/gogantic/agent" + "github.com/bit8bytes/gogantic/llm/openai" + "github.com/bit8bytes/gogantic/runner" + "github.com/bit8bytes/gogantic/tool" +) + +const ( + ToolGetTemperatureInFahrenheit = "get_temperature_in_fahrenheit" + ToolFormatFahrenheitToCelsius = "format_fahrenheit_to_celsius" +) + +type GetTemperatureInFahrenheit struct{} +type FormatFahrenheitToCelsius struct{} + +func main() { + apiKey := os.Getenv("OPENAI_API_KEY") + if len(apiKey) == 0 { + log.Fatal("No OPENAI_API_KEY") + } + + stream := false + stop := []string{"\nObservation", "Observation"} + + gpt_35_turbo := openai.Model{ + Model: "gpt-3.5-turbo", + APIKey: apiKey, + Stream: &stream, + Stop: &stop, + } + + llm := openai.New(gpt_35_turbo) + + tools := map[string]tool.Tool{ + ToolGetTemperatureInFahrenheit: GetTemperatureInFahrenheit{}, + ToolFormatFahrenheitToCelsius: FormatFahrenheitToCelsius{}, + } + + weatherAgent := agent.New(llm, tools) + weatherAgent.Task("What is the temperature outside?") + + runner := runner.New(weatherAgent, + runner.WithIterationLimit(10), + runner.WithShowMessages()) + runner.Run(context.TODO()) + + finalAnswer, _ := weatherAgent.GetFinalAnswer() + fmt.Println(finalAnswer) +} + +func (t GetTemperatureInFahrenheit) Name() string { + return ToolGetTemperatureInFahrenheit +} + +func (t GetTemperatureInFahrenheit) Schema() string { return `()` } + +func (t GetTemperatureInFahrenheit) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + // This is only for showcase. + // If you want to use this and handle input e.g. location look at the math agent example. + return tool.Output{Content: "15.54°F"}, nil +} + +func (t FormatFahrenheitToCelsius) Name() string { + return ToolFormatFahrenheitToCelsius +} + +func (t FormatFahrenheitToCelsius) Schema() string { return `(0000)` } + +func (t FormatFahrenheitToCelsius) Call(ctx context.Context, input tool.Input) (tool.Output, error) { + // Still, I do not handle errors in here. This has to be done through testing. + fahrenheit, _ := strconv.ParseFloat(input.Content, 64) + celsius := (fahrenheit - 32) * (5.0 / 9.0) + return tool.Output{Content: fmt.Sprintf("Current temperature: %.2f°C", celsius)}, nil +} diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 0000000..4a52aee --- /dev/null +++ b/runner/runner.go @@ -0,0 +1,88 @@ +package runner + +import ( + "context" + "fmt" + + "github.com/bit8bytes/gogantic/agent" +) + +const ( + Reset = "\033[0m" + Bold = "\033[1m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + White = "\033[37m" + BgRed = "\033[41m" + BgGreen = "\033[42m" + BgYellow = "\033[43m" + BgBlue = "\033[44m" + BgMagenta = "\033[45m" + BgCyan = "\033[46m" +) + +type Runner struct { + Agent *agent.Agent + IterationLimit int + printMessages bool +} + +type RunnerOption func(*Runner) + +func WithIterationLimit(limit int) RunnerOption { + return func(e *Runner) { + e.IterationLimit = limit + } +} + +func WithShowMessages() RunnerOption { + return func(e *Runner) { + e.printMessages = true + } +} + +func New(agent *agent.Agent, opts ...RunnerOption) *Runner { + e := &Runner{ + Agent: agent, + IterationLimit: 10, + printMessages: false, + } + + for _, opt := range opts { + opt(e) + } + + return e +} + +// Updated Run function for your Runner +func (e *Runner) Run(ctx context.Context) { + for i := 1; i < e.IterationLimit; i++ { + todos, err := e.Agent.Plan(ctx) + if err != nil { + fmt.Println("Error planning:", err) + break + } + + if todos.Finish { + break + } + + e.Agent.Act(ctx) + + if e.printMessages && len(e.Agent.Messages) > 0 { + thought := fmt.Sprintf("%s: %s", e.Agent.Messages[len(e.Agent.Messages)-4].Role, e.Agent.Messages[len(e.Agent.Messages)-4].Content) + action := fmt.Sprintf("%s: %s", e.Agent.Messages[len(e.Agent.Messages)-3].Role, e.Agent.Messages[len(e.Agent.Messages)-3].Content) + actionInput := fmt.Sprintf("%s: %s", e.Agent.Messages[len(e.Agent.Messages)-2].Role, e.Agent.Messages[len(e.Agent.Messages)-2].Content) + observation := fmt.Sprintf("%s: %s", e.Agent.Messages[len(e.Agent.Messages)-1].Role, e.Agent.Messages[len(e.Agent.Messages)-1].Content) + fmt.Println(Blue + thought + Reset) + fmt.Println(Yellow + action + Reset) + fmt.Println(Yellow + actionInput + Reset) + fmt.Println(Green + observation + Reset) + } + } +} diff --git a/tool/tool.go b/tool/tool.go new file mode 100644 index 0000000..d790480 --- /dev/null +++ b/tool/tool.go @@ -0,0 +1,19 @@ +package tool + +import ( + "context" +) + +type Input struct { + Content string +} + +type Output struct { + Content string +} + +type Tool interface { + Name() string + Call(ctx context.Context, input Input) (Output, error) + Schema() string +} From d1eba54c5e76026cf563cd27b9f98da83e73942a Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Fri, 6 Feb 2026 07:45:30 -0500 Subject: [PATCH 2/4] chore: rm error on new() --- agent/agent.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 6f42a39..eb99210 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -27,6 +27,8 @@ func getToolNames(tools map[string]tool.Tool) string { return strings.Join(names, ", ") } +type Options func(*Agent) + func New(model llm.LLM, tools map[string]tool.Tool) *Agent { toolNames := getToolNames(tools) initialMessages := setupReActPromptInitialMessages(toolNames) @@ -40,7 +42,7 @@ func New(model llm.LLM, tools map[string]tool.Tool) *Agent { // Task the agent is going to execute func (a *Agent) Task(prompt string) { - chatPrompt, _ := chat.New([]llm.Message{{Role: "user", Content: "Question: {{.input}}\n"}}) + chatPrompt := chat.New([]llm.Message{{Role: "user", Content: "Question: {{.input}}\n"}}) data := map[string]any{"input": prompt} @@ -157,7 +159,7 @@ func (a *Agent) GetFinalAnswer() (string, error) { } func setupReActPromptInitialMessages(tools string) []llm.Message { - reActPrompt, _ := chat.New([]llm.Message{ + reActPrompt := chat.New([]llm.Message{ {Role: "user", Content: ` Answer the following questions as best you can. Use only values from the tools. Do not estimate or predict values. From 6e61b8d9ec56b05afd658956af6c4df8f748feed Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sat, 7 Feb 2026 20:27:45 -0500 Subject: [PATCH 3/4] refactor: inputs, llms, outputs, and pipe --- .github/dependabot.yml | 15 + .github/instructions/daisyui.instructions.md | 1682 +++++++++++++++++ .github/workflows/sec_scan.yml | 31 + .github/workflows/tests.yml | 36 + README.md | 30 +- agent/agent.go => agents/agents.go | 58 +- {agent => agents}/messages.go | 12 +- agents/tools/tools.go | 9 + {agent => agents}/types.go | 2 +- docs/EXAMPLES.md | 18 +- docs/img/banner.png | Bin 471948 -> 0 bytes docs/img/gogantic-mascot.png | Bin 1937171 -> 0 bytes docs/img/logo.png | Bin 1507834 -> 0 bytes embedder/embedder.go | 20 +- examples/agent/openai/temperature/main.go | 82 - .../ollama/filesystem/altered_foobar.txt | 0 .../ollama/filesystem/foobar.txt | 0 .../ollama/filesystem/main.go | 32 +- .../ollama/temperature/main.go | 18 +- .../{agent => agents}/ollama/time/main.go | 14 +- examples/input/chat/main.go | 32 - examples/input/prompt/main.go | 23 - examples/inputs/chats/main.go | 43 + examples/inputs/prompts/main.go | 28 + examples/llm/ollama/main.go | 45 - examples/llm/openai/embeddings/main.go | 43 - examples/llm/openai/main.go | 53 - .../{llm => llms}/ollama/embeddings/main.go | 8 +- examples/llms/ollama/main.go | 53 + examples/{llm => llms}/ollama/stream/main.go | 10 +- examples/output/json/main.go | 27 - examples/output/markdown/main.go | 23 - examples/output/markdown/markdown.md | 58 - examples/output/replacer/replacer.go | 24 - examples/output/separator/separator.go | 31 - examples/outputs/jsonout/main.go | 30 + examples/pipe/json/main.go | 55 - examples/pipe/main.go | 50 - examples/pipe/markdown/main.go | 52 - examples/pipes/json/main.go | 65 + input/chat/chat_templates.go | 40 - input/chat/chat_templates_test.go | 61 - input/prompt/prompt_templates.go | 31 - inputs/chats/chats_templates.go | 50 + inputs/chats/chats_templates_test.go | 80 + inputs/prompts/prompts_templates.go | 27 + .../prompts/prompts_templates_test.go | 9 +- inputs/roles/roles.go | 8 + llm/llm.go | 26 - llm/openai/openai.go | 159 -- llm/openai/openai_types.go | 70 - llms/llms.go | 18 + {llm => llms}/ollama/ollama.go | 34 +- {llm => llms}/ollama/ollama_types.go | 8 +- output/json/json_parser.go | 50 - output/maps/maps_parser.go | 28 - output/markdown/markdown_parser.go | 46 - output/parser_interface.go | 10 - output/replacer/replacer_parser.go | 29 - output/separator/separator_parser.go | 77 - outputs/json/jsonout.go | 39 + pipe/pipe.go | 59 - pipes/pipes.go | 47 + runner/runner.go | 7 +- tool/tool.go | 19 - 65 files changed, 2397 insertions(+), 1447 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/instructions/daisyui.instructions.md create mode 100644 .github/workflows/sec_scan.yml create mode 100644 .github/workflows/tests.yml rename agent/agent.go => agents/agents.go (81%) rename {agent => agents}/messages.go (73%) create mode 100644 agents/tools/tools.go rename {agent => agents}/types.go (94%) delete mode 100644 docs/img/banner.png delete mode 100644 docs/img/gogantic-mascot.png delete mode 100644 docs/img/logo.png delete mode 100644 examples/agent/openai/temperature/main.go rename examples/{agent => agents}/ollama/filesystem/altered_foobar.txt (100%) rename examples/{agent => agents}/ollama/filesystem/foobar.txt (100%) rename examples/{agent => agents}/ollama/filesystem/main.go (66%) rename examples/{agent => agents}/ollama/temperature/main.go (80%) rename examples/{agent => agents}/ollama/time/main.go (69%) delete mode 100644 examples/input/chat/main.go delete mode 100644 examples/input/prompt/main.go create mode 100644 examples/inputs/chats/main.go create mode 100644 examples/inputs/prompts/main.go delete mode 100644 examples/llm/ollama/main.go delete mode 100644 examples/llm/openai/embeddings/main.go delete mode 100644 examples/llm/openai/main.go rename examples/{llm => llms}/ollama/embeddings/main.go (66%) create mode 100644 examples/llms/ollama/main.go rename examples/{llm => llms}/ollama/stream/main.go (80%) delete mode 100644 examples/output/json/main.go delete mode 100644 examples/output/markdown/main.go delete mode 100644 examples/output/markdown/markdown.md delete mode 100644 examples/output/replacer/replacer.go delete mode 100644 examples/output/separator/separator.go create mode 100644 examples/outputs/jsonout/main.go delete mode 100644 examples/pipe/json/main.go delete mode 100644 examples/pipe/main.go delete mode 100644 examples/pipe/markdown/main.go create mode 100644 examples/pipes/json/main.go delete mode 100644 input/chat/chat_templates.go delete mode 100644 input/chat/chat_templates_test.go delete mode 100644 input/prompt/prompt_templates.go create mode 100644 inputs/chats/chats_templates.go create mode 100644 inputs/chats/chats_templates_test.go create mode 100644 inputs/prompts/prompts_templates.go rename input/prompt/prompt_templates_test.go => inputs/prompts/prompts_templates_test.go (90%) create mode 100644 inputs/roles/roles.go delete mode 100644 llm/llm.go delete mode 100644 llm/openai/openai.go delete mode 100644 llm/openai/openai_types.go create mode 100644 llms/llms.go rename {llm => llms}/ollama/ollama.go (78%) rename {llm => llms}/ollama/ollama_types.go (88%) delete mode 100644 output/json/json_parser.go delete mode 100644 output/maps/maps_parser.go delete mode 100644 output/markdown/markdown_parser.go delete mode 100644 output/parser_interface.go delete mode 100644 output/replacer/replacer_parser.go delete mode 100644 output/separator/separator_parser.go create mode 100644 outputs/json/jsonout.go delete mode 100644 pipe/pipe.go create mode 100644 pipes/pipes.go delete mode 100644 tool/tool.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..031ede8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/.github/workflows" + schedule: + interval: "weekly" diff --git a/.github/instructions/daisyui.instructions.md b/.github/instructions/daisyui.instructions.md new file mode 100644 index 0000000..9b16f97 --- /dev/null +++ b/.github/instructions/daisyui.instructions.md @@ -0,0 +1,1682 @@ +--- +description: daisyUI 5 +alwaysApply: true +applyTo: "**" +--- + +# daisyUI 5 +daisyUI 5 is a CSS library for Tailwind CSS 4 +daisyUI 5 provides class names for common UI components + +- [daisyUI 5 docs](http://daisyui.com) +- [Guide: How to use this file in LLMs and code editors](https://daisyui.com/docs/editor/) +- [daisyUI 5 release notes](https://daisyui.com/docs/v5/) +- [daisyUI 4 to 5 upgrade guide](https://daisyui.com/docs/upgrade/) + +## daisyUI 5 install notes +[install guide](https://daisyui.com/docs/install/) +1. daisyUI 5 requires Tailwind CSS 4 +2. `tailwind.config.js` file is deprecated in Tailwind CSS v4. do not use `tailwind.config.js`. Tailwind CSS v4 only needs `@import "tailwindcss";` in the CSS file if it's a node dependency. +3. daisyUI 5 can be installed using `npm i -D daisyui@latest` and then adding `@plugin "daisyui";` to the CSS file +4. daisyUI is suggested to be installed as a dependency but if you really want to use it from CDN, you can use Tailwind CSS and daisyUI CDN files: +```html + + +``` +5. A CSS file with Tailwind CSS and daisyUI looks like this (if it's a node dependency) +```css +@import "tailwindcss"; +@plugin "daisyui"; +``` + +## daisyUI 5 usage rules +1. We can give styles to a HTML element by adding daisyUI class names to it. By adding a component class name, part class names (if there's any available for that component), and modifier class names (if there's any available for that component) +2. Components can be customized using Tailwind CSS utility classes if the customization is not possible using the existing daisyUI classes. For example `btn px-10` sets a custom horizontal padding to a `btn` +3. If customization of daisyUI styles using Tailwind CSS utility classes didn't work because of CSS specificity issues, you can use the `!` at the end of the Tailwind CSS utility class to override the existing styles. For example `btn bg-red-500!` sets a custom background color to a `btn` forcefully. This is a last resort solution and should be used sparingly +4. If a specific component or something similar to it doesn't exist in daisyUI, you can create your own component using Tailwind CSS utility +5. when using Tailwind CSS `flex` and `grid` for layout, it should be responsive using Tailwind CSS responsive utility prefixes. +6. Only allowed class names are existing daisyUI class names or Tailwind CSS utility classes. +7. Ideally, you won't need to write any custom CSS. Using daisyUI class names or Tailwind CSS utility classes is preferred. +8. suggested - if you need placeholder images, use https://picsum.photos/200/300 with the size you want +9. suggested - when designing , don't add a custom font unless it's necessary +10. don't add `bg-base-100 text-base-content` to body unless it's necessary +11. For design decisions, use Refactoring UI book best practices + +daisyUI 5 class names are one of the following categories. these type names are only for reference and are not used in the actual code +- `component`: the required component class +- `part`: a child part of a component +- `style`: sets a specific style to component or part +- `behavior`: changes the behavior of component or part +- `color`: sets a specific color to component or part +- `size`: sets a specific size to component or part +- `placement`: sets a specific placement to component or part +- `direction`: sets a specific direction to component or part +- `modifier`: modifies the component or part in a specific way + +## Config +daisyUI 5 config docs: https://daisyui.com/docs/config/ +daisyUI without config: +```css +@plugin "daisyui"; +``` +daisyUI config with `light` theme only: +```css +@plugin "daisyui" { + themes: light --default; +} +``` +daisyUI with all the default configs: +```css +@plugin "daisyui" { + themes: light --default, dark --prefersdark; + root: ":root"; + include: ; + exclude: ; + prefix: ; + logs: true; +} +``` +An example config: +In below config, all the built-in themes are enabled while bumblebee is the default theme and synthwave is the prefersdark theme (default dark mode) +All the other themes are enabled and can be used by adding `data-theme="THEME_NAME"` to the `` element +root scrollbar gutter is excluded. `daisy-` prefix is used for all daisyUI classes and console.log is disabled +```css +@plugin "daisyui" { + themes: light, dark, cupcake, bumblebee --default, emerald, corporate, synthwave --prefersdark, retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel, fantasy, wireframe, black, luxury, dracula, cmyk, autumn, business, acid, lemonade, night, coffee, winter, dim, nord, sunset, caramellatte, abyss, silk; + root: ":root"; + include: ; + exclude: rootscrollgutter, checkbox; + prefix: daisy-; + logs: false; +} +``` +## daisyUI 5 colors + +### daisyUI color names +- `primary`: Primary brand color, The main color of your brand +- `primary-content`: Foreground content color to use on primary color +- `secondary`: Secondary brand color, The optional, secondary color of your brand +- `secondary-content`: Foreground content color to use on secondary color +- `accent`: Accent brand color, The optional, accent color of your brand +- `accent-content`: Foreground content color to use on accent color +- `neutral`: Neutral dark color, For not-saturated parts of UI +- `neutral-content`: Foreground content color to use on neutral color +- `base-100`:-100 Base surface color of page, used for blank backgrounds +- `base-200`:-200 Base color, darker shade, to create elevations +- `base-300`:-300 Base color, even more darker shade, to create elevations +- `base-content`: Foreground content color to use on base color +- `info`: Info color, For informative/helpful messages +- `info-content`: Foreground content color to use on info color +- `success`: Success color, For success/safe messages +- `success-content`: Foreground content color to use on success color +- `warning`: Warning color, For warning/caution messages +- `warning-content`: Foreground content color to use on warning color +- `error`: Error color, For error/danger/destructive messages +- `error-content`: Foreground content color to use on error color + +### daisyUI color rules +1. daisyUI adds semantic color names to Tailwind CSS colors +2. daisyUI color names can be used in utility classes, like other Tailwind CSS color names. for example, `bg-primary` will use the primary color for the background +3. daisyUI color names include variables as value so they can change based the theme +4. There's no need to use `dark:` for daisyUI color names +5. Ideally only daisyUI color names should be used for colors so the colors can change automatically based on the theme +6. If a Tailwind CSS color name (like `red-500`) is used, it will be same red color on all themes +7. If a daisyUI color name (like `primary`) is used, it will change color based on the theme +8. Using Tailwind CSS color names for text colors should be avoided because Tailwind CSS color `text-gray-800` on `bg-base-100` would be unreadable on a dark theme - because on dark theme, `bg-base-100` is a dark color +9. `*-content` colors should have a good contrast compared to their associated colors +10. suggestion - when designing a page use `base-*` colors for majority of the page. use `primary` color for important elements + +### daisyUI custom theme with custom colors +A CSS file with Tailwind CSS, daisyUI and a custom daisyUI theme looks like this: +```css +@import "tailwindcss"; +@plugin "daisyui"; +@plugin "daisyui/theme" { + name: "mytheme"; + default: true; /* set as default */ + prefersdark: false; /* set as default dark mode (prefers-color-scheme:dark) */ + color-scheme: light; /* color of browser-provided UI */ + + --color-base-100: oklch(98% 0.02 240); + --color-base-200: oklch(95% 0.03 240); + --color-base-300: oklch(92% 0.04 240); + --color-base-content: oklch(20% 0.05 240); + --color-primary: oklch(55% 0.3 240); + --color-primary-content: oklch(98% 0.01 240); + --color-secondary: oklch(70% 0.25 200); + --color-secondary-content: oklch(98% 0.01 200); + --color-accent: oklch(65% 0.25 160); + --color-accent-content: oklch(98% 0.01 160); + --color-neutral: oklch(50% 0.05 240); + --color-neutral-content: oklch(98% 0.01 240); + --color-info: oklch(70% 0.2 220); + --color-info-content: oklch(98% 0.01 220); + --color-success: oklch(65% 0.25 140); + --color-success-content: oklch(98% 0.01 140); + --color-warning: oklch(80% 0.25 80); + --color-warning-content: oklch(20% 0.05 80); + --color-error: oklch(65% 0.3 30); + --color-error-content: oklch(98% 0.01 30); + + --radius-selector: 1rem; /* border radius of selectors (checkbox, toggle, badge) */ + --radius-field: 0.25rem; /* border radius of fields (button, input, select, tab) */ + --radius-box: 0.5rem; /* border radius of boxes (card, modal, alert) */ + /* preferred values for --radius-* : 0rem, 0.25rem, 0.5rem, 1rem, 2rem */ + + --size-selector: 0.25rem; /* base size of selectors (checkbox, toggle, badge). Value must be 0.25rem unless we intentionally want bigger selectors. In so it can be 0.28125 or 0.3125. If we intentionally want smaller selectors, it can be 0.21875 or 0.1875 */ + --size-field: 0.25rem; /* base size of fields (button, input, select, tab). Value must be 0.25rem unless we intentionally want bigger fields. In so it can be 0.28125 or 0.3125. If we intentionally want smaller fields, it can be 0.21875 or 0.1875 */ + + --border: 1px; /* border size. Value must be 1px unless we intentionally want thicker borders. In so it can be 1.5px or 2px. If we intentionally want thinner borders, it can be 0.5px */ + + --depth: 1; /* only 0 or 1 – Adds a shadow and subtle 3D depth effect to components */ + --noise: 0; /* only 0 or 1 - Adds a subtle noise (grain) effect to components */ +} +``` +#### Rules +- All CSS variables above are required +- Colors can be OKLCH or hex or other formats +- If you're generating a custom theme, do not include the comments from the example above. Just provide the code. + +People can use https://daisyui.com/theme-generator/ visual tool to create their own theme. + +## daisyUI 5 components + +### accordion +Accordion is used for showing and hiding content but only one item can stay open at a time + +[accordion docs](https://daisyui.com/components/accordion/) + +#### Class names +- component: `collapse` +- part: `collapse-title`, `collapse-content` +- modifier: `collapse-arrow`, `collapse-plus`, `collapse-open`, `collapse-close` + +#### Syntax +```html +
{CONTENT}
+``` +where content is: +```html + +
{title}
+
{CONTENT}
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier class names +- Accordion uses radio inputs. All radio inputs with the same name work together and only one of them can be open at a time +- If you have more than one set of accordion items on a page, use different names for the radio inputs on each set +- Replace {name} with a unique name for the accordion group +- replace `{checked}` with `checked="checked"` if you want the accordion to be open by default + +### alert +Alert informs users about important events + +[alert docs](https://daisyui.com/components/alert/) + +#### Class names +- component: `alert` +- style: `alert-outline`, `alert-dash`, `alert-soft` +- color: `alert-info`, `alert-success`, `alert-warning`, `alert-error` +- direction: `alert-vertical`, `alert-horizontal` + +#### Syntax +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/direction class names +- Add `sm:alert-horizontal` for responsive layouts + +### avatar +Avatars are used to show a thumbnail + +[avatar docs](https://daisyui.com/components/avatar/) + +#### Class names +- component: `avatar`, `avatar-group` +- modifier: `avatar-online`, `avatar-offline`, `avatar-placeholder` + +#### Syntax +```html +
+
+ +
+
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier class names +- Use `avatar-group` for containing multiple avatars +- You can set custom sizes using `w-*` and `h-*` +- You can use mask classes such as `mask-squircle`, `mask-hexagon`, `mask-triangle` + +### badge +Badges are used to inform the user of the status of specific data + +[badge docs](https://daisyui.com/components/badge/) + +#### Class names +- component: `badge` +- style: `badge-outline`, `badge-dash`, `badge-soft`, `badge-ghost` +- color: `badge-neutral`, `badge-primary`, `badge-secondary`, `badge-accent`, `badge-info`, `badge-success`, `badge-warning`, `badge-error` +- size: `badge-xs`, `badge-sm`, `badge-md`, `badge-lg`, `badge-xl` + +#### Syntax +```html +Badge +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/size class names +- Can be used inside text or buttons +- To create an empty badge, just remove the text between the span tags + +### breadcrumbs +Breadcrumbs helps users to navigate + +[breadcrumbs docs](https://daisyui.com/components/breadcrumbs/) + +#### Class names +- component: `breadcrumbs` + +#### Syntax +```html + +``` + +#### Rules +- breadcrumbs only has one main class name +- Can contain icons inside the links +- If you set `max-width` or the list gets larger than the container it will scroll + +### button +Buttons allow the user to take actions + +[button docs](https://daisyui.com/components/button/) + +#### Class names +- component: `btn` +- color: `btn-neutral`, `btn-primary`, `btn-secondary`, `btn-accent`, `btn-info`, `btn-success`, `btn-warning`, `btn-error` +- style: `btn-outline`, `btn-dash`, `btn-soft`, `btn-ghost`, `btn-link` +- behavior: `btn-active`, `btn-disabled` +- size: `btn-xs`, `btn-sm`, `btn-md`, `btn-lg`, `btn-xl` +- modifier: `btn-wide`, `btn-block`, `btn-square`, `btn-circle` + +#### Syntax +```html + +``` +#### Rules +- {MODIFIER} is optional and can have one of each color/style/behavior/size/modifier class names +- btn can be used on any html tags such as ` +``` + +#### Rules +- {MODIFIER} is optional and can have one of the size class names +- To make a button active, add `dock-active` class to the button +- add `` is required for responsivness of the dock in iOS + +### drawer +Drawer is a grid layout that can show/hide a sidebar on the left or right side of the page + +[drawer docs](https://daisyui.com/components/drawer/) + +#### Class names +- component: `drawer` +- part: `drawer-toggle`, `drawer-content`, `drawer-side`, `drawer-overlay` +- placement: `drawer-end` +- modifier: `drawer-open` + +#### Syntax +```html +
+ +
{CONTENT}
+
{SIDEBAR}
+
+``` +where {CONTENT} can be navbar, site content, footer, etc +and {SIDEBAR} can be a menu like: +```html +
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier/placement class names +- `id` is required for the `drawer-toggle` input. change `my-drawer` to a unique id according to your needs +- `lg:drawer-open` can be used to make sidebar visible on larger screens +- `drawer-toggle` is a hidden checkbox. Use label with "for" attribute to toggle state +- if you want to open the drawer when a button is clicked, use `` where `my-drawer` is the id of the `drawer-toggle` input +- when using drawer, every page content must be inside `drawer-content` element. for example navbar, footer, etc should not be outside of `drawer` + +### dropdown +Dropdown can open a menu or any other element when the button is clicked + +[dropdown docs](https://daisyui.com/components/dropdown/) + +#### Class names +- component: `dropdown` +- part: `dropdown-content` +- placement: `dropdown-start`, `dropdown-center`, `dropdown-end`, `dropdown-top`, `dropdown-bottom`, `dropdown-left`, `dropdown-right` +- modifier: `dropdown-hover`, `dropdown-open` + +#### Syntax +Using details and summary +```html + +``` + +Using popover API +```html + + +``` + +Using CSS focus +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier/placement class names +- replace `{id}` and `{anchor}` with a unique name +- For CSS focus dropdowns, use `tabindex="0"` and `role="button"` on the button +- The content can be any HTML element (not just `