diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..920cace --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +GOCMD=go + +tests: + $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --flake-attempts 5 --fail-fast -v -r ./... \ No newline at end of file diff --git a/agent/agent_suite_test.go b/agent/agent_suite_test.go new file mode 100644 index 0000000..ff10f4c --- /dev/null +++ b/agent/agent_suite_test.go @@ -0,0 +1,13 @@ +package agent_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAgent(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Agent test suite") +} diff --git a/agent/constructor.go b/agent/constructor.go new file mode 100644 index 0000000..7eed122 --- /dev/null +++ b/agent/constructor.go @@ -0,0 +1,101 @@ +package agent + +import ( + "github.com/mudler/local-agent-framework/llm" + "github.com/sashabaranov/go-openai" +) + +type llmOptions struct { + APIURL string + Model string +} + +type options struct { + LLMAPI llmOptions + Character Character +} + +type Agent struct { + options *options + Character Character + client *openai.Client +} + +type Option func(*options) error + +func defaultOptions() *options { + return &options{ + LLMAPI: llmOptions{ + APIURL: "http://localhost:8080", + Model: "echidna", + }, + Character: Character{ + Name: "John Doe", + Age: 0, + Occupation: "Unemployed", + NowDoing: "Nothing", + DoingNext: "Nothing", + DoneHistory: []string{}, + Memories: []string{}, + Hobbies: []string{}, + MusicTaste: []string{}, + }, + } +} + +func newOptions(opts ...Option) (*options, error) { + options := defaultOptions() + for _, o := range opts { + if err := o(options); err != nil { + return nil, err + } + } + return options, nil +} + +func New(opts ...Option) (*Agent, error) { + options, err := newOptions(opts...) + if err != nil { + return nil, err + } + + client := llm.NewClient("", options.LLMAPI.APIURL) + a := &Agent{ + options: options, + client: client, + Character: options.Character, + } + return a, nil +} + +func WithLLMAPIURL(url string) Option { + return func(o *options) error { + o.LLMAPI.APIURL = url + return nil + } +} + +func WithModel(model string) Option { + return func(o *options) error { + o.LLMAPI.Model = model + return nil + } +} + +func WithCharacter(c Character) Option { + return func(o *options) error { + o.Character = c + return nil + } +} + +func FromFile(path string) Option { + return func(o *options) error { + c, err := Load(path) + if err != nil { + return err + } + o.Character = *c + return nil + } +} diff --git a/agent/state.go b/agent/state.go new file mode 100644 index 0000000..9a45669 --- /dev/null +++ b/agent/state.go @@ -0,0 +1,48 @@ +package agent + +import ( + "encoding/json" + "os" + + "github.com/mudler/local-agent-framework/llm" +) + +type Character struct { + Name string `json:"name"` + Age int `json:"age"` + Occupation string `json:"job_occupation"` + NowDoing string `json:"doing_now"` + DoingNext string `json:"doing_next"` + DoneHistory []string `json:"done_history"` + Memories []string `json:"memories"` + Hobbies []string `json:"hobbies"` + MusicTaste []string `json:"music_taste"` +} + +func Load(path string) (*Character, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var c Character + err = json.Unmarshal(data, &c) + if err != nil { + return nil, err + } + return &c, nil +} + +func (a *Agent) Save(path string) error { + data, err := json.Marshal(a.options.Character) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +func (a *Agent) GenerateIdentity(guidance string) error { + err := llm.GenerateJSONFromStruct(a.client, guidance, a.options.LLMAPI.Model, &a.options.Character) + + a.Character = a.options.Character + return err +} diff --git a/agent/state_test.go b/agent/state_test.go new file mode 100644 index 0000000..9700ee0 --- /dev/null +++ b/agent/state_test.go @@ -0,0 +1,34 @@ +package agent_test + +import ( + "fmt" + + . "github.com/mudler/local-agent-framework/agent" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Agent test", func() { + + Context("identity", func() { + + It("generates all the fields", func() { + agent, err := New( + WithLLMAPIURL("http://192.168.68.113:8080"), + WithModel("echidna")) + Expect(err).ToNot(HaveOccurred()) + err = agent.GenerateIdentity("An old man with a long beard, a wizard, who lives in a tower.") + Expect(err).ToNot(HaveOccurred()) + Expect(agent.Character.Name).ToNot(BeEmpty()) + Expect(agent.Character.Age).ToNot(BeZero()) + Expect(agent.Character.Occupation).ToNot(BeEmpty()) + Expect(agent.Character.NowDoing).ToNot(BeEmpty()) + Expect(agent.Character.DoingNext).ToNot(BeEmpty()) + Expect(agent.Character.DoneHistory).ToNot(BeEmpty()) + Expect(agent.Character.Memories).ToNot(BeEmpty()) + Expect(agent.Character.Hobbies).ToNot(BeEmpty()) + Expect(agent.Character.MusicTaste).ToNot(BeEmpty()) + fmt.Printf("%+v\n", agent.Character) + }) + }) +}) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6e7b9ef --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/mudler/local-agent-framework + +go 1.21.1 + +require ( + github.com/onsi/ginkgo/v2 v2.15.0 + github.com/onsi/gomega v1.31.1 + github.com/sashabaranov/go-openai v1.18.3 +) + +require ( + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.16.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..500deec --- /dev/null +++ b/go.sum @@ -0,0 +1,44 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= +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/sashabaranov/go-openai v1.18.3 h1:dspFGkmZbhjg1059KhqLYSV2GaCiRIn+bOu50TlXUq8= +github.com/sashabaranov/go-openai v1.18.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/llm/client.go b/llm/client.go new file mode 100644 index 0000000..10cba75 --- /dev/null +++ b/llm/client.go @@ -0,0 +1,14 @@ +package llm + +import "github.com/sashabaranov/go-openai" + +func NewClient(APIKey, URL string) *openai.Client { + // Set up OpenAI client + if APIKey == "" { + //log.Fatal("OPENAI_API_KEY environment variable not set") + APIKey = "sk-xxx" + } + config := openai.DefaultConfig(APIKey) + config.BaseURL = URL + return openai.NewClientWithConfig(config) +} diff --git a/llm/json.go b/llm/json.go new file mode 100644 index 0000000..7d41915 --- /dev/null +++ b/llm/json.go @@ -0,0 +1,47 @@ +package llm + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/sashabaranov/go-openai" +) + +// generateAnswer generates an answer for the given text using the OpenAI API +func GenerateJSON(client *openai.Client, model, text string, i interface{}) error { + req := openai.ChatCompletionRequest{ + ResponseFormat: &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject}, + Model: model, + Messages: []openai.ChatCompletionMessage{ + { + + Role: "user", + Content: text, + }, + }, + } + + resp, err := client.CreateChatCompletion(context.Background(), req) + if err != nil { + return fmt.Errorf("failed to generate answer: %v", err) + } + if len(resp.Choices) == 0 { + return fmt.Errorf("no response from OpenAI API") + } + + err = json.Unmarshal([]byte(resp.Choices[0].Message.Content), i) + if err != nil { + return err + } + return nil +} + +func GenerateJSONFromStruct(client *openai.Client, guidance, model string, i interface{}) error { + // TODO: use functions? + exampleJSON, err := json.Marshal(i) + if err != nil { + return err + } + return GenerateJSON(client, model, "Generate a character as JSON data. "+guidance+". This is the JSON fields that should contain: "+string(exampleJSON), i) +}