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
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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"
1,682 changes: 1,682 additions & 0 deletions .github/instructions/daisyui.instructions.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions .github/workflows/sec_scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Security Scan
on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
security:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout Source
uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"

- name: Run Gosec Security Scanner
uses: securego/gosec@master

- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
36 changes: 36 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test cmd/web

on:
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read

steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'

- name: Download dependencies
run: go mod download

- name: Format code
run: go fmt ./...

- name: Vet code
run: go vet ./...

- name: Run tests
run: go test -race -vet=off ./...

- name: Test coverage
run: |
go test -covermode=count -coverprofile=coverage.out ./...
19 changes: 7 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# Interacting with LLMs in Go has never been easier.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Test](https://github.com/bit8bytes/gogantic/actions/workflows/test_cmd_app.yml/badge.svg)](https://github.com/bit8bytes/gogantic/actions/workflows/tests.yml)

Meet Gogo the Giant Gopher.
Meet Gogo the standing chick. 🐥.

<p align="center"> <img src="/docs/img/gogantic-mascot.png" alt="Gogantic Mascot" width="250"/></p>

Gogo helps you work with LLMs in Go(lang) — without external dependencies.
Gogo speeds up your interactions with LLMs while keeping your stack lean and efficient.

## 🚴🏽‍♂️ Roadmap

See [Roadmap](/docs/GET-INVOLVED.md#%EF%B8%8F-roadmap).
* Gogo helps you work with LLMs in Go(lang) — without external dependencies
* Gogo keeps your stack lean and efficient
* Gogo can be run everywhere using minimal resources pointing to a LLM

## Example:

Expand All @@ -25,10 +20,10 @@ fmt.Println("Translate from", result.InputLanguage, " to ", result.OutputLanguag
fmt.Println("Result: ", result.Text)
```

Go to [Examples](/docs/EXAMPLES.md) for more info.

## 📚 Sources and Inspiration

Note, these inspirations have different goals then Gogo but are worth looking into.

- [tmc/langchaingo](https://github.com/tmc/langchaingo)

## ✨ Contributors
Expand Down
244 changes: 244 additions & 0 deletions agents/agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package agents

import (
"context"
"errors"
"fmt"
"strings"

"github.com/bit8bytes/gogantic/agents/tools"
"github.com/bit8bytes/gogantic/inputs/chats"
"github.com/bit8bytes/gogantic/llms"
)

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

type Tool interface {
Name() string
Call(ctx context.Context, input tools.Input) (tools.Output, error)
}

type Agent struct {
Model LLM
Tools map[string]Tool
Messages []llms.Message
Actions []Action
}

func getToolNames(tools map[string]Tool) string {
names := ""
for _, tool := range tools {
names += ", " + tool.Name()
}
return names
}

type Options func(*Agent)

func New(model LLM, tools map[string]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 := chats.New([]llms.Message{{Role: "user", Content: "Question: {{.input}}\n"}})

data := map[string]any{"input": prompt}

formattedMessages, err := chatPrompt.Execute(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.Generate(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, llms.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 := tools.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) []llms.Message {
reActPrompt := chats.New([]llms.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.Execute(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 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
}
Loading
Loading