Skip to content

obot-platform/go-hash-tool

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Writing your first tool in Go

go-hash-tool contains a reference Go implementation of the Hash tool.

This guide walks through the structure and design of the tool and outlines the packaging requirements for Obot

To clone this repo and follow along, run the following command:

git clone [email protected]:obot-platform/go-hash-tool

Tool Repo Structure

The directory tree below highlights the files required to implement Hash in Go and package it for Obot.

go-hash-tool
├── tool.gpt
├── go.mod
├── main.go
└── commands
    └── hash.go

Note: Most tools implemented in Go will also have a go.sum, which is required if the tool has dependencies. It is not present in the reference implementation because it has no external dependencies and relies solely on the Go standard library.


Defining the Hash Tool

The tool.gpt file contains GPTScript Tool Definitions which describe a set of tools that can be used by agents in Obot. Every tool repository must have a tool.gpt file in its root directory.

The tools defined in this file must have a Name and Description that will help agents understand what the tool does, what it returns (if anything), and all the Parameters it takes. Agents use these details to infer a tool's usage. We call the section of a tool definition that contains this info a Preamble.

We want the Hash tool to return the hash of some given data. It would also be nice to support a few different algorithms for the agent to choose from. Let's take a look at the Preamble for Hash to see how that's achieved:

Name: Hash
Description: Generate a hash of data using the given algorithm and return the result as a hexadecimal string
Param: data: The data to hash
Param: algo: The algorithm to generate a hash with. Supports "sha256" and "md5". Default is "sha256"

Breaking this down a bit:

  • The Preamble above declares a tool named Hash.
  • The Param fields enumerate the arguments that an agent must provide when calling Hash, data and algo.
  • In this case, the description of the algo parameter outlines the valid options (sha256 or md5) and defines a default value (sha256)
  • The Description explains what Hash returns with respect to the given arguments; the hash of data using the algorithm selected with algo.

Immediately below the Preamble is the Tool Body, which tells Obot how to execute the tool:

#!{GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool hash

This is where the magic happens.

To simplify, when an agent calls the Hash tool, Obot reads this line and then:

  1. Downloads the appropriate Go toolchain
  2. Sets up a working directory for the tool
  3. Runs go build to install dependencies (from go.mod and go.sum) and build a binary named gptscript-go-tool (gptscript-go-tool.exe on Windows)
  4. Projects the call arguments onto environment variables (DATA and ALGO)
  5. Runs gptscript-go-tool hash

Putting it all together, here's the complete definition of the Hash tool:

Name: Hash
Description: Generate a hash of data using the given algorithm and return the result as a hexadecimal string
Param: data: The data to hash
Param: algo: The algorithm to generate a hash with. Default is "sha256". Supports "sha256" and "md5".

#!{GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool hash

Tool Metadata

The tool.gpt file also provides the following metadata for use in Obot:

  • !metadata:*:category which tags tools with the Crypto category to promote organization and discovery
  • !metadata:*:icon which assigns https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg as the tool icon

Note: * is a wildcard pattern that applies the metadata to all tools in the tool.gpt file.

---
!metadata:*:category
Crypto

---
!metadata:*:icon
https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg
Note: Metadata can be applied to a specific tool by either specifying the exact name (e.g. !metadata:Hash:category) or by adding the metadata directly to a tool's Preamble
Name: Hash
Metadata: category: Crypto
Metadata: icon: https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg

Complete tool.gpt
---
Name: Hash
Description: Generate a hash of data using the given algorithm and return the result as a hexadecimal string
Param: data: The data to hash
Param: algo: The algorithm to generate a hash with. Supports "sha256" and "md5". Default is "sha256"

#!{GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool hash

---
!metadata:*:category
Crypto

---
!metadata:*:icon
https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg

Implementing Business Logic

The main.go file is the entry point of the gptscript-go-tool binary that is executed by Obot when the Hash tool is called.

Let's walk through the code to understand what happens at runtime:

// ...
switch cmd := os.Args[0]; cmd {
case "hash":
    res, err = commands.Hash(os.Getenv("DATA"), os.Getenv("ALGO"))
default:
    err = fmt.Errorf("Unsupported command: %s", cmd)
}

if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

if res != "" {
    fmt.Println(res)
}

This code implements a simple CLI responsible for dispatching the commands.Hash function on request -- when hash is passed in as an argument -- after extracting the tool arguments, data and algo, from the respective environment variables.

It also ensures that the return value and errors of the call to commands.Hash are written to stdout. This is crucial because only stdout is returned to the agent, while stderr is discarded.

Note: The simple CLI pattern showcased above is also easily extensible; adding business logic for new tools becomes a matter of adding a new case to the switch statement.

For example, to add business logic for a new tool to verify a hash, we just have to tack on verify case:

// ...
case "verify":
     res, err = commands.Verify(os.Getenv("HASH"), os.Getenv("DATA"), os.Getenv("ALGO"))
case "hash":
    // ...
default:
    //...

The Body of the Verify tool definition would then simply pass verify to gptscript-go-tool instead of hash:

Name: Verify
# ...

#!{GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool verify

The commands.Hash function implements the bulk of the Hash tool's business logic.

It starts off by validating the data and algo arguments.

func Hash(data, algo string) (string, error) {
    if data == "" {
        return "", fmt.Errorf("A non-empty data argument must be provided")
    }

    if algo == "" {
        algo = "sha256"
    }

    sum, ok := hashFunctions[algo]
	if !ok {
		return "", fmt.Errorf("Unsupported hash algorithm: %s not in [%s]", algo, hashFunctions)
	}
    // ...

When an argument is invalid, the function returns an error that describes the validation issue in detail. The goal is to provide useful information that an agent can use to construct valid arguments for future calls. For example, when an invalid algo argument is provided, the code returns an error that contains the complete list of valid algorithms.


Once it determines that all the arguments are valid, it calculates the hash and writes a JSON object to stdout. This object contains both the hash and the algorithm used to generate it.

    // ...
    hash, err := json.Marshal(hashResult{
		Algo: algo,
		Hash: hex.EncodeToString(sum([]byte(data))),
	})
	if err != nil {
		return "", fmt.Errorf("Failed to marshal hash result: %w", err)
	}

	return string(hash), nil
}

Note: Producing structured data with extra contextual info (e.g. the algorithm) is considered good form. It's a pattern that improves the agent's ability to correctly use the tool's result over time.

Complete main.go and hash.go
// main.go
package main

import (
	"fmt"
	"os"

	"github.com/obot-platform/go-hash-tool/commands"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: gptscript-go-tool <command>")
		os.Exit(1)
	}

	var (
		err error
		res string
	)
	switch cmd := os.Args[1]; cmd {
	case "hash":
		res, err = commands.Hash(os.Getenv("DATA"), os.Getenv("ALGO"))
	default:
		err = fmt.Errorf("Unsupported command: %s", cmd)
	}

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	if res != "" {
		fmt.Println(res)
	}
}
// commands/hash.go
package commands

import (
	"crypto/md5"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
)

func Hash(data, algo string) (string, error) {
	if data == "" {
		return "", fmt.Errorf("A non-empty data argument must be provided")
	}

	if algo == "" {
		algo = "sha256"
	}

	sum, ok := hashFunctions[algo]
	if !ok {
		return "", fmt.Errorf("Unsupported hash algorithm: %s not in [%s]", algo, hashFunctions)
	}

	hash, err := json.Marshal(hashResult{
		Algo: algo,
		Hash: hex.EncodeToString(sum([]byte(data))),
	})
	if err != nil {
		return "", fmt.Errorf("Failed to marshal hash result: %w", err)
	}

	return string(hash), nil
}

type hashResult struct {
	Algo string `json:"algo"`
	Hash string `json:"hash"`
}

var hashFunctions = hashFuncSet{
	"sha256": func(d []byte) []byte { h := sha256.Sum256(d); return h[:] },
	"md5":    func(d []byte) []byte { h := md5.Sum(d); return h[:] },
}

type hashFuncSet map[string]func([]byte) []byte

func (s hashFuncSet) String() string {
	return strings.Join(keys(s), ", ")
}

func keys[V any](m map[string]V) []string {
	set := make([]string, 0, len(m))
	for k := range m {
		set = append(set, k)
	}

	sort.Strings(set)
	return set
}

Testing main.go Locally

Before adding a tool to Obot, verify that the Go business logic works on your machine.

To do this, run through the following steps in the root of your local fork:

  1. Install dependencies and build the binary

    make build
  2. Run the tool with some test arguments:

    Command Output
    DATA='foo' bin/gptscript-go-tool hash { "algo": "sha256", "hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" }
    DATA='' bin/gptscript-go-tool hash Error: A data argument must be provided
    DATA='foo' ALGO='md5' bin/gptscript-go-tool hash { "algo": "md5", "hash": "acbd18db4cc2f85cedef654fccc4a4d8" }
    DATA='foo' ALGO='whirlpool' bin/gptscript-go-tool hash Error: Unsupported hash algorithm: whirlpool not in ['sha256', 'md5']

Adding the Hash tool to Obot

Before a tool can be used by an agent, an admin must first add the tool to Obot by performing the steps below:

  1. Navigate to the Obot admin UI in a browser and open the Tools page by clicking the Tools button in the left drawer
    Open The Tools Page
  2. Click the Register New Tool button on the right
    Click The Register New Tool Button
  3. Type the tool repo reference into the modal's input box and click Register Tool
    Enter Tool Repo Reference

Once the tool has been added, you can search for it by category or name on the Tools page to verify
Search For Newly Added Tools

Using The Hash tool in an agent

To use the Hash tool in an agent, open the agent's Edit page, then:

  1. Click the Add Tool button under either the Agent Tools or User Tools sections
    Click The Add Tool Button
  2. Search for "Hash" or "Crypto" in the tool search pop-out and select the Hash tool
    Add Hash Tool To Agent
  3. Ask the agent to generate a hash
    Ask The Agent To Generate a Hash

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published