From 59e369ca8b51244800665f32c9a9c88824f986e3 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Sat, 31 May 2025 16:26:11 -0700 Subject: [PATCH 1/8] Initial commit --- .gitignore | 3 + flake.lock | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 50 ++++++++++ 3 files changed, 315 insertions(+) create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e7de68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.devenv +.envrc +agent diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3560984 --- /dev/null +++ b/flake.lock @@ -0,0 +1,262 @@ +{ + "nodes": { + "cachix": { + "inputs": { + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv" + ], + "git-hooks": [ + "devenv" + ], + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1744206633, + "narHash": "sha256-pb5aYkE8FOoa4n123slgHiOf1UbNSnKe5pEZC+xXD5g=", + "owner": "cachix", + "repo": "cachix", + "rev": "8a60090640b96f9df95d1ab99e5763a586be1404", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", + "nix": "nix", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748688634, + "narHash": "sha256-S9J86Dn05c1Skk1dpOEUM2AgO+tkiAyaGXT2PZLMcRM=", + "owner": "cachix", + "repo": "devenv", + "rev": "df38744269cc4ea8cee9e36fe882e9ef786df11b", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "devenv" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746537231, + "narHash": "sha256-Wb2xeSyOsCoTCTj7LOoD6cdKLEROyFAArnYoS+noCWo=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "fa466640195d38ec97cf0493d6d6882bc4d14969", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "libgit2": { + "flake": false, + "locked": { + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", + "type": "github" + }, + "original": { + "owner": "libgit2", + "repo": "libgit2", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": [ + "devenv" + ], + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ], + "pre-commit-hooks": [ + "devenv" + ] + }, + "locked": { + "lastModified": 1745930071, + "narHash": "sha256-bYyjarS3qSNqxfgc89IoVz8cAFDkF9yPE63EJr+h50s=", + "owner": "domenkozar", + "repo": "nix", + "rev": "b455edf3505f1bf0172b39a735caef94687d0d9c", + "type": "github" + }, + "original": { + "owner": "domenkozar", + "ref": "devenv-2.24", + "repo": "nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1746807397, + "narHash": "sha256-zU2z0jlkJGWLhdNr/8AJSxqK8XD0IlQgHp3VZcP56Aw=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "c5208b594838ea8e6cca5997fbf784b7cca1ca90", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs_3", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d0978fe --- /dev/null +++ b/flake.nix @@ -0,0 +1,50 @@ +{ + inputs = { + nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; + systems.url = "github:nix-systems/default"; + devenv.url = "github:cachix/devenv"; + devenv.inputs.nixpkgs.follows = "nixpkgs"; + }; + + nixConfig = { + extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="; + extra-substituters = "https://devenv.cachix.org"; + }; + + outputs = + { + self, + nixpkgs, + devenv, + systems, + ... + }@inputs: + let + forEachSystem = nixpkgs.lib.genAttrs (import systems); + in + { + packages = forEachSystem (system: { + devenv-up = self.devShells.${system}.default.config.procfileScript; + devenv-test = self.devShells.${system}.default.config.test; + }); + + devShells = forEachSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = devenv.lib.mkShell { + inherit inputs pkgs; + modules = [ + { + # https://devenv.sh/reference/options/ + languages.go.enable = true; + languages.javascript.enable = true; + } + ]; + }; + } + ); + }; +} From 920f59e630502fda601a3e8fba9e9fcc3a93ba60 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Tue, 3 Jun 2025 22:05:37 -0700 Subject: [PATCH 2/8] Add conversation loop --- go.mod | 12 +++++++++ go.sum | 12 +++++++++ main.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b51242c --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module agent + +go 1.24.2 + +require github.com/anthropics/anthropic-sdk-go v1.3.0 + +require ( + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a7142b --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/anthropics/anthropic-sdk-go v1.3.0 h1:KroW4oDT3KzFT71d3bnu4DxLFAEPvY+d1c6z2CrOz/s= +github.com/anthropics/anthropic-sdk-go v1.3.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b1443f4 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-sdk-go" +) + +func main() { + client := anthropic.NewClient() + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + agent := NewAgent(&client, getUserMessage) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + } +} + +type Agent struct { + client *anthropic.Client + getUserMessage func() (string, bool) +} + +func (a *Agent) Run(ctx context.Context) error { + conversation := []anthropic.MessageParam{} + + fmt.Println("Chat with Claude (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + break + } + + userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) + conversation = append(conversation, userMessage) + + message, err := a.runInference(ctx, conversation) + if err != nil { + return err + } + conversation = append(conversation, message.ToParam()) + + for _, content := range message.Content { + switch content.Type { + case "text": + fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) + } + } + } + + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { + message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{ + Model: anthropic.ModelClaude3_7SonnetLatest, + MaxTokens: int64(1024), + Messages: conversation, + }) + return message, err +} From 9a00f00d4786aae8aef388eb4af89c3737762ecf Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Tue, 3 Jun 2025 22:13:25 -0700 Subject: [PATCH 3/8] Added support for tools --- main.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index b1443f4..0fc7f59 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "context" + "encoding/json" "fmt" "os" @@ -19,24 +20,27 @@ func main() { } return scanner.Text(), true } + tools := []ToolDefinition{} - agent := NewAgent(&client, getUserMessage) + agent := NewAgent(&client, getUserMessage, tools) err := agent.Run(context.TODO()) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } } -func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent { +func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool), tools []ToolDefinition) *Agent { return &Agent{ client: client, getUserMessage: getUserMessage, + tools: tools, } } type Agent struct { client *anthropic.Client getUserMessage func() (string, bool) + tools []ToolDefinition } func (a *Agent) Run(ctx context.Context) error { @@ -72,10 +76,29 @@ func (a *Agent) Run(ctx context.Context) error { } func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { + anthropicTools := []anthropic.ToolUnionParam{} + for _, tool := range a.tools { + anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{ + OfTool: &anthropic.ToolParam{ + Name: tool.Name, + Description: anthropic.String(tool.Description), + InputSchema: tool.InputSchema, + }, + }) + } + message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaude3_7SonnetLatest, MaxTokens: int64(1024), Messages: conversation, + Tools: anthropicTools, }) return message, err } + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"` + Function func(input json.RawMessage) (string, error) +} From c7b16b9f2ab3276eb39688be5ef37ab7f60e49f9 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Tue, 3 Jun 2025 22:19:07 -0700 Subject: [PATCH 4/8] Add read file tool --- go.mod | 10 ++++++- go.sum | 21 +++++++++++++ main.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index b51242c..6d7a035 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,19 @@ module agent go 1.24.2 -require github.com/anthropics/anthropic-sdk-go v1.3.0 +require ( + github.com/anthropics/anthropic-sdk-go v1.3.0 + github.com/invopop/jsonschema v0.13.0 +) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3a7142b..6de236b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,20 @@ github.com/anthropics/anthropic-sdk-go v1.3.0 h1:KroW4oDT3KzFT71d3bnu4DxLFAEPvY+d1c6z2CrOz/s= github.com/anthropics/anthropic-sdk-go v1.3.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -10,3 +25,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 0fc7f59..f01db02 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os" "github.com/anthropics/anthropic-sdk-go" + "github.com/invopop/jsonschema" ) func main() { @@ -20,7 +21,7 @@ func main() { } return scanner.Text(), true } - tools := []ToolDefinition{} + tools := []ToolDefinition{ReadFileDefinition} agent := NewAgent(&client, getUserMessage, tools) err := agent.Run(context.TODO()) @@ -48,15 +49,18 @@ func (a *Agent) Run(ctx context.Context) error { fmt.Println("Chat with Claude (use 'ctrl-c' to quit)") + readUserInput := true for { - fmt.Print("\u001b[94mYou\u001b[0m: ") - userInput, ok := a.getUserMessage() - if !ok { - break - } + if readUserInput { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + break + } - userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) - conversation = append(conversation, userMessage) + userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) + conversation = append(conversation, userMessage) + } message, err := a.runInference(ctx, conversation) if err != nil { @@ -64,17 +68,49 @@ func (a *Agent) Run(ctx context.Context) error { } conversation = append(conversation, message.ToParam()) + toolResults := []anthropic.ContentBlockParamUnion{} for _, content := range message.Content { switch content.Type { case "text": fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) + case "tool_use": + result := a.executeTool(content.ID, content.Name, content.Input) + toolResults = append(toolResults, result) } } + if len(toolResults) == 0 { + readUserInput = true + continue + } + readUserInput = false + conversation = append(conversation, anthropic.NewUserMessage(toolResults...)) } return nil } +func (a *Agent) executeTool(id, name string, input json.RawMessage) anthropic.ContentBlockParamUnion { + var toolDef ToolDefinition + var found bool + for _, tool := range a.tools { + if tool.Name == name { + toolDef = tool + found = true + break + } + } + if !found { + return anthropic.NewToolResultBlock(id, "tool not found", true) + } + + fmt.Printf("\u001b[92mtool\u001b[0m: %s(%s)\n", name, input) + response, err := toolDef.Function(input) + if err != nil { + return anthropic.NewToolResultBlock(id, err.Error(), true) + } + return anthropic.NewToolResultBlock(id, response, false) +} + func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { anthropicTools := []anthropic.ToolUnionParam{} for _, tool := range a.tools { @@ -102,3 +138,44 @@ type ToolDefinition struct { InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"` Function func(input json.RawMessage) (string, error) } + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + InputSchema: ReadFileInputSchema, + Function: ReadFile, +} + +type ReadFileInput struct { + Path string `json:"path" jsonschema_description:"The relative path of a file in the working directory."` +} + +var ReadFileInputSchema = GenerateSchema[ReadFileInput]() + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + panic(err) + } + + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + return "", err + } + return string(content), nil +} + +func GenerateSchema[T any]() anthropic.ToolInputSchemaParam { + reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + DoNotReference: true, + } + var v T + + schema := reflector.Reflect(v) + + return anthropic.ToolInputSchemaParam{ + Properties: schema.Properties, + } +} From 8c15c4768b6c8cb932d647ef3f2d9fe494727974 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Sat, 31 May 2025 16:26:11 -0700 Subject: [PATCH 5/8] Initial commit --- .envrc | 15 +++ .gitignore | 3 + flake.lock | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 50 ++++++++++ 4 files changed, 330 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..77eccda --- /dev/null +++ b/.envrc @@ -0,0 +1,15 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" +fi + +watch_file flake.nix +watch_file flake.lock +if ! use flake . --no-pure-eval +then + echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 +fi + +# NOTE: Add a line like this to .env +# export ANTHROPIC_API_KEY=XXX +dotenv + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f5ec81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.devenv +.env +agent diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3560984 --- /dev/null +++ b/flake.lock @@ -0,0 +1,262 @@ +{ + "nodes": { + "cachix": { + "inputs": { + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv" + ], + "git-hooks": [ + "devenv" + ], + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1744206633, + "narHash": "sha256-pb5aYkE8FOoa4n123slgHiOf1UbNSnKe5pEZC+xXD5g=", + "owner": "cachix", + "repo": "cachix", + "rev": "8a60090640b96f9df95d1ab99e5763a586be1404", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", + "nix": "nix", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748688634, + "narHash": "sha256-S9J86Dn05c1Skk1dpOEUM2AgO+tkiAyaGXT2PZLMcRM=", + "owner": "cachix", + "repo": "devenv", + "rev": "df38744269cc4ea8cee9e36fe882e9ef786df11b", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "devenv" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746537231, + "narHash": "sha256-Wb2xeSyOsCoTCTj7LOoD6cdKLEROyFAArnYoS+noCWo=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "fa466640195d38ec97cf0493d6d6882bc4d14969", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "libgit2": { + "flake": false, + "locked": { + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", + "type": "github" + }, + "original": { + "owner": "libgit2", + "repo": "libgit2", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": [ + "devenv" + ], + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ], + "pre-commit-hooks": [ + "devenv" + ] + }, + "locked": { + "lastModified": 1745930071, + "narHash": "sha256-bYyjarS3qSNqxfgc89IoVz8cAFDkF9yPE63EJr+h50s=", + "owner": "domenkozar", + "repo": "nix", + "rev": "b455edf3505f1bf0172b39a735caef94687d0d9c", + "type": "github" + }, + "original": { + "owner": "domenkozar", + "ref": "devenv-2.24", + "repo": "nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1746807397, + "narHash": "sha256-zU2z0jlkJGWLhdNr/8AJSxqK8XD0IlQgHp3VZcP56Aw=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "c5208b594838ea8e6cca5997fbf784b7cca1ca90", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs_3", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d0978fe --- /dev/null +++ b/flake.nix @@ -0,0 +1,50 @@ +{ + inputs = { + nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; + systems.url = "github:nix-systems/default"; + devenv.url = "github:cachix/devenv"; + devenv.inputs.nixpkgs.follows = "nixpkgs"; + }; + + nixConfig = { + extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="; + extra-substituters = "https://devenv.cachix.org"; + }; + + outputs = + { + self, + nixpkgs, + devenv, + systems, + ... + }@inputs: + let + forEachSystem = nixpkgs.lib.genAttrs (import systems); + in + { + packages = forEachSystem (system: { + devenv-up = self.devShells.${system}.default.config.procfileScript; + devenv-test = self.devShells.${system}.default.config.test; + }); + + devShells = forEachSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = devenv.lib.mkShell { + inherit inputs pkgs; + modules = [ + { + # https://devenv.sh/reference/options/ + languages.go.enable = true; + languages.javascript.enable = true; + } + ]; + }; + } + ); + }; +} From 9d238ea9dcdf66ce344c77b9900937e7aeffb3b8 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Tue, 3 Jun 2025 22:05:37 -0700 Subject: [PATCH 6/8] Add conversation loop --- go.mod | 12 +++++++++ go.sum | 12 +++++++++ main.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b51242c --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module agent + +go 1.24.2 + +require github.com/anthropics/anthropic-sdk-go v1.3.0 + +require ( + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a7142b --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/anthropics/anthropic-sdk-go v1.3.0 h1:KroW4oDT3KzFT71d3bnu4DxLFAEPvY+d1c6z2CrOz/s= +github.com/anthropics/anthropic-sdk-go v1.3.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b1443f4 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-sdk-go" +) + +func main() { + client := anthropic.NewClient() + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + agent := NewAgent(&client, getUserMessage) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + } +} + +type Agent struct { + client *anthropic.Client + getUserMessage func() (string, bool) +} + +func (a *Agent) Run(ctx context.Context) error { + conversation := []anthropic.MessageParam{} + + fmt.Println("Chat with Claude (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + break + } + + userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) + conversation = append(conversation, userMessage) + + message, err := a.runInference(ctx, conversation) + if err != nil { + return err + } + conversation = append(conversation, message.ToParam()) + + for _, content := range message.Content { + switch content.Type { + case "text": + fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) + } + } + } + + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { + message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{ + Model: anthropic.ModelClaude3_7SonnetLatest, + MaxTokens: int64(1024), + Messages: conversation, + }) + return message, err +} From 74feac4e255837337e594689360785dcc2d181f6 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Tue, 3 Jun 2025 22:13:25 -0700 Subject: [PATCH 7/8] Added support for tools --- main.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index b1443f4..0fc7f59 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "context" + "encoding/json" "fmt" "os" @@ -19,24 +20,27 @@ func main() { } return scanner.Text(), true } + tools := []ToolDefinition{} - agent := NewAgent(&client, getUserMessage) + agent := NewAgent(&client, getUserMessage, tools) err := agent.Run(context.TODO()) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } } -func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent { +func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool), tools []ToolDefinition) *Agent { return &Agent{ client: client, getUserMessage: getUserMessage, + tools: tools, } } type Agent struct { client *anthropic.Client getUserMessage func() (string, bool) + tools []ToolDefinition } func (a *Agent) Run(ctx context.Context) error { @@ -72,10 +76,29 @@ func (a *Agent) Run(ctx context.Context) error { } func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { + anthropicTools := []anthropic.ToolUnionParam{} + for _, tool := range a.tools { + anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{ + OfTool: &anthropic.ToolParam{ + Name: tool.Name, + Description: anthropic.String(tool.Description), + InputSchema: tool.InputSchema, + }, + }) + } + message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaude3_7SonnetLatest, MaxTokens: int64(1024), Messages: conversation, + Tools: anthropicTools, }) return message, err } + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"` + Function func(input json.RawMessage) (string, error) +} From 6fefd684320bae60ffa75e2abeb321f376eb92c3 Mon Sep 17 00:00:00 2001 From: alejandro-angulo Date: Tue, 3 Jun 2025 22:19:07 -0700 Subject: [PATCH 8/8] Add read file tool --- go.mod | 10 ++++++- go.sum | 21 +++++++++++++ main.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index b51242c..6d7a035 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,19 @@ module agent go 1.24.2 -require github.com/anthropics/anthropic-sdk-go v1.3.0 +require ( + github.com/anthropics/anthropic-sdk-go v1.3.0 + github.com/invopop/jsonschema v0.13.0 +) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3a7142b..6de236b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,20 @@ github.com/anthropics/anthropic-sdk-go v1.3.0 h1:KroW4oDT3KzFT71d3bnu4DxLFAEPvY+d1c6z2CrOz/s= github.com/anthropics/anthropic-sdk-go v1.3.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -10,3 +25,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 0fc7f59..f01db02 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os" "github.com/anthropics/anthropic-sdk-go" + "github.com/invopop/jsonschema" ) func main() { @@ -20,7 +21,7 @@ func main() { } return scanner.Text(), true } - tools := []ToolDefinition{} + tools := []ToolDefinition{ReadFileDefinition} agent := NewAgent(&client, getUserMessage, tools) err := agent.Run(context.TODO()) @@ -48,15 +49,18 @@ func (a *Agent) Run(ctx context.Context) error { fmt.Println("Chat with Claude (use 'ctrl-c' to quit)") + readUserInput := true for { - fmt.Print("\u001b[94mYou\u001b[0m: ") - userInput, ok := a.getUserMessage() - if !ok { - break - } + if readUserInput { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + break + } - userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) - conversation = append(conversation, userMessage) + userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) + conversation = append(conversation, userMessage) + } message, err := a.runInference(ctx, conversation) if err != nil { @@ -64,17 +68,49 @@ func (a *Agent) Run(ctx context.Context) error { } conversation = append(conversation, message.ToParam()) + toolResults := []anthropic.ContentBlockParamUnion{} for _, content := range message.Content { switch content.Type { case "text": fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) + case "tool_use": + result := a.executeTool(content.ID, content.Name, content.Input) + toolResults = append(toolResults, result) } } + if len(toolResults) == 0 { + readUserInput = true + continue + } + readUserInput = false + conversation = append(conversation, anthropic.NewUserMessage(toolResults...)) } return nil } +func (a *Agent) executeTool(id, name string, input json.RawMessage) anthropic.ContentBlockParamUnion { + var toolDef ToolDefinition + var found bool + for _, tool := range a.tools { + if tool.Name == name { + toolDef = tool + found = true + break + } + } + if !found { + return anthropic.NewToolResultBlock(id, "tool not found", true) + } + + fmt.Printf("\u001b[92mtool\u001b[0m: %s(%s)\n", name, input) + response, err := toolDef.Function(input) + if err != nil { + return anthropic.NewToolResultBlock(id, err.Error(), true) + } + return anthropic.NewToolResultBlock(id, response, false) +} + func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) { anthropicTools := []anthropic.ToolUnionParam{} for _, tool := range a.tools { @@ -102,3 +138,44 @@ type ToolDefinition struct { InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"` Function func(input json.RawMessage) (string, error) } + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + InputSchema: ReadFileInputSchema, + Function: ReadFile, +} + +type ReadFileInput struct { + Path string `json:"path" jsonschema_description:"The relative path of a file in the working directory."` +} + +var ReadFileInputSchema = GenerateSchema[ReadFileInput]() + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + panic(err) + } + + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + return "", err + } + return string(content), nil +} + +func GenerateSchema[T any]() anthropic.ToolInputSchemaParam { + reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + DoNotReference: true, + } + var v T + + schema := reflector.Reflect(v) + + return anthropic.ToolInputSchemaParam{ + Properties: schema.Properties, + } +}