diff --git a/eino/example/chapter2/main.go b/eino/example/chapter2/main.go index 96de01e..f0b1eac 100644 --- a/eino/example/chapter2/main.go +++ b/eino/example/chapter2/main.go @@ -87,7 +87,7 @@ func main() { // Introducing agent agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: "Ch02ChatModelAgent", - Description: "A minimal ChatAgent with in-memory multi-turn history.", + Description: "A minimal ChatAgent with in-memory multi-turn history. NEVER use emojis.", Instruction: instruction, Model: cm, }) diff --git a/eino/example/chapter3/MUTEX.md b/eino/example/chapter3/MUTEX.md new file mode 100644 index 0000000..7f7dc48 --- /dev/null +++ b/eino/example/chapter3/MUTEX.md @@ -0,0 +1,5 @@ +# Notes on Mutexs + +Remember that you can't read and write to a map across different threads. Same with arrays or slices. To avoid clobbering your data you can use a mutex to gaurd operations. + +In this implemtation of the SessionStore needs a Mutex for reading and writing from the cache. The Session object needs it's own mutex to append new entries to the messages slice, and to safely flush that to the session file on disk. diff --git a/eino/example/chapter3/README.md b/eino/example/chapter3/README.md new file mode 100644 index 0000000..8ad4cb0 --- /dev/null +++ b/eino/example/chapter3/README.md @@ -0,0 +1,5 @@ +# Eino Agent Example Chapter 1 + +https://www.cloudwego.io/docs/eino/quick_start/chapter_01_chatmodel_and_message/ + +Simple hello world example of writing an Agent in Golang. diff --git a/eino/example/chapter3/go.mod b/eino/example/chapter3/go.mod new file mode 100644 index 0000000..ecb146d --- /dev/null +++ b/eino/example/chapter3/go.mod @@ -0,0 +1,44 @@ +module chapter1 + +go 1.26.1 + +require ( + github.com/cloudwego/eino v0.8.6 + github.com/cloudwego/eino-ext/components/model/openai v0.1.11 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/eino-ext/libs/acl/openai v0.1.15 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eino-contrib/jsonschema v1.0.3 // indirect + github.com/evanphx/json-patch v0.5.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/meguminnnnnnnnn/go-openai v0.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nikolalohinski/gonja v1.5.3 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/sys v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/eino/example/chapter3/go.sum b/eino/example/chapter3/go.sum new file mode 100644 index 0000000..128fb9a --- /dev/null +++ b/eino/example/chapter3/go.sum @@ -0,0 +1,151 @@ +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +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/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +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/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/mockey v1.3.0 h1:ONLRdvhqmCfr9rTasUB8ZKCfvbdD2tohOg4u+4Q/ed0= +github.com/bytedance/mockey v1.3.0/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/eino v0.8.6 h1:Rc9/ElXNrTrSCv68t/U0yUmNVu5uMmpPyMCb+WyFIQQ= +github.com/cloudwego/eino v0.8.6/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU= +github.com/cloudwego/eino-ext/components/model/openai v0.1.11 h1:juf9kECfmxJBA0rJSxDT7XOUSgrbMHaGHgBwd06lYaI= +github.com/cloudwego/eino-ext/components/model/openai v0.1.11/go.mod h1:DBk44Dq1mhuoAacdUzzhZhSGeeBECDI2rIZnJFeVZoE= +github.com/cloudwego/eino-ext/libs/acl/openai v0.1.15 h1:LbdSG9+qWzzp9RFW6dSFkaUW171JvCoYn/K63zX6dQE= +github.com/cloudwego/eino-ext/libs/acl/openai v0.1.15/go.mod h1:p+l0zBB0GjjX8HTlbTs3g3KfUFwZC11bsCGZOXW/3L0= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= +github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA= +github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +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= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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/eino/example/chapter3/main.go b/eino/example/chapter3/main.go new file mode 100644 index 0000000..f0b1eac --- /dev/null +++ b/eino/example/chapter3/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "bufio" + "context" + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/cloudwego/eino-ext/components/model/openai" + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/schema" +) + +// sourceEnv a crude .env file reader +func sourceEnv() { + file, err := os.Open(".env") + if err != nil { + fmt.Println(".env not found or cannot be read") + return + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + split_env := strings.Split(scanner.Text(), "=") + if len(split_env) != 2 { + log.Fatal(".env file expexted '=' to delimit key and value. Check .env file for proper format.") + } + fmt.Printf("Setting from .env: %s\n", split_env[0]) + os.Setenv(split_env[0], split_env[1]) + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + +} + +type AgentConfig struct { + BaseUrl string + ModelId string + ApiKey string +} + +func NewAgentConfig() *AgentConfig { + url, ok := os.LookupEnv("OPENAI_BASE_URL") + if !ok { + log.Fatal("An OPENAI_BASE_URL must be specified as an environment variable.") + } + + modelId, ok := os.LookupEnv("OPENAI_API_MODEL") + if !ok { + log.Fatal("A model id must be specified with OPENAI_API_MODEL as an environment variable.") + } + + apiKey, ok := os.LookupEnv("OPENAI_API_KEY") + if !ok { + fmt.Println("No API found as OPENAI_API_KEY. Using dummy value") + apiKey = "dummyvalue" + } + return &AgentConfig{url, modelId, apiKey} +} + +func main() { + sourceEnv() + var instruction string + flag.StringVar(&instruction, "instruction", "You are a helpful assistant.", "Set a system prompt for your agent.") + flag.Parse() + + ctx := context.Background() + agentCfg := NewAgentConfig() + cm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{ + Model: agentCfg.ModelId, + BaseURL: agentCfg.BaseUrl, + APIKey: agentCfg.ApiKey, + }) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // Introducing agent + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "Ch02ChatModelAgent", + Description: "A minimal ChatAgent with in-memory multi-turn history. NEVER use emojis.", + Instruction: instruction, + Model: cm, + }) + + // A runner manages the agent's life cyle including start up, interruption. + // Are a unified entrypoint of an agent and enables additional features like checkpoints for interruption + /// recovery. I also converts the Agent's Event Stream into a consumable AysncIterator[*AgentEvent] + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + + history := make([]*schema.Message, 0, 16) + scanner := bufio.NewScanner(os.Stdin) + + // Note there is an alternative runner.Query method which will also produce an AysncIterator[*AgentEvent] + // Each Run innvocation creates a new iterator. Iterators cannot be reused once consumed. + + for { + _, _ = fmt.Fprint(os.Stdout, "you> ") + if !scanner.Scan() { + break + } + line := strings.TrimSpace(scanner.Text()) + if line == "" { + break + } + + history = append(history, schema.UserMessage(line)) + + events := runner.Run(ctx, history) + + content, err := printAndCollectAssistantFromEvents(events) + if err != nil { + log.Fatal(err) + } + history = append(history, schema.AssistantMessage(content, nil)) + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + +} + +func printAndCollectAssistantFromEvents(events *adk.AsyncIterator[*adk.AgentEvent]) (string, error) { + var sb strings.Builder + + for { + event, ok := events.Next() + if !ok { + break + } + + if event.Err != nil { + return "", event.Err + } + + if event.Output == nil || event.Output.MessageOutput == nil { + continue + } + + msgVariant := event.Output.MessageOutput + if msgVariant.Role != schema.Assistant { + continue + } + + if msgVariant.IsStreaming { + // + msgVariant.MessageStream.SetAutomaticClose() + for { + frame, err := msgVariant.MessageStream.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return "", err + } + if frame != nil && frame.Content != "" { + sb.WriteString(frame.Content) + _, _ = fmt.Fprint(os.Stdout, frame.Content) + } + } + _, _ = fmt.Fprintln(os.Stdout) + continue + } + + if msgVariant.Message != nil { + sb.WriteString(msgVariant.Message.Content) + _, _ = fmt.Fprintln(os.Stdout, msgVariant.Message.Content) + } else { + _, _ = fmt.Fprint(os.Stdout) + } + } + + return sb.String(), nil +} diff --git a/eino/example/chapter3/store.go b/eino/example/chapter3/store.go new file mode 100644 index 0000000..8c3faf4 --- /dev/null +++ b/eino/example/chapter3/store.go @@ -0,0 +1,288 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/cloudwego/eino/schema" +) + +// SessionMetadata provides summary information for sessions +type SessionMetadata struct { + ID string `json:"id"` + Title string `json:"title"` + CreatedAt time.Time `json:"created_at"` + MessageCount int `json:"message_count"` +} + +// A SessionHeader is the first line in a JSON session file. +type SessionHeader struct { + Type string `json:"type"` + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` +} + +// Session holds the in-memory state for a single chat conversation. +type Session struct { + ID string + CreatedAt time.Time + FilePath string + mu sync.Mutex + Messages []*schema.Message +} + +// Append adds a message to the session and flushes it to disk +func (s *Session) Append(msg *schema.Message) error { + s.mu.Lock() + defer s.mu.Unlock() + + // append to in memory structure + s.Messages = append(s.Messages, msg) + + data, err := json.Marshal(msg) + if err != nil { + return err + } + + // open the file for appending + f, err := os.OpenFile(s.FilePath, os.O_APPEND|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + + _, err = fmt.Fprintf(f, "%s\n", data) + return err +} + +// GetMessages returns a snapshot of all session messages +func (s *Session) GetMessages() []*schema.Message { + s.mu.Lock() + defer s.mu.Unlock() + + snapshot := make([]*schema.Message, len(s.Messages)) + copy(snapshot, s.Messages) + return snapshot +} + +// Title produces a display title from the first user message of a session. +func (s *Session) Title() string { + s.mu.Lock() + defer s.mu.Unlock() + + for _, msg := range s.Messages { + if msg.Role == schema.User && msg.Content != "" { + title := msg.Content + if len([]rune(title)) > 60 { + title = string([]rune(title))[:60] + "..." + } + return title + } + } + return "New Session..." +} + +// Store manages persisted sessions backed by JSONL files. +// +// The firstline of a Session file is a SessionHeader line. +// All subsequent lines are schema.Message contents. +// +// File format example: +// +// {"type":"session","id":"...","created_at":"..."} +// {"role":"user","content":"..."} +type SessionStore struct { + dir string + mu sync.Mutex + cache map[string]*Session +} + +// GetOrCreate will retrieve a session from disk or create a new session +// and persist its metadata to disk. +func (ss *SessionStore) GetOrCreate(id string) (*Session, error) { + ss.mu.Lock() + defer ss.mu.Unlock() + + var ( + session *Session + err error + ok bool + ) + + session, ok = ss.cache[id] + if ok { + return session, nil + } + + filePath := filepath.Join(ss.dir, id+".jsonl") + + if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) { + session, err = createSession(id, filePath) + } else { + session, err = loadSession(filePath) + } + if err != nil { + return nil, err + } + ss.cache[id] = session + + return session, nil +} + +// List returns the metadata for all sessions. +func (ss *SessionStore) List() ([]SessionMetadata, error) { + ss.mu.Lock() + defer ss.mu.Unlock() + + entries, err := os.ReadDir(ss.dir) + if err != nil { + return nil, err + } + + var meta []SessionMetadata + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { + continue + } + id := strings.TrimSuffix(e.Name(), ".jsonl") + session, ok := ss.cache[id] + if ok { + meta = append(meta, SessionMetadata{ID: id, Title: session.Title(), CreatedAt: session.CreatedAt, MessageCount: len(session.Messages)}) + continue + } else { + session, err = loadSession(filepath.Join(ss.dir, e.Name())) + if err != nil { + continue + } + meta = append(meta, SessionMetadata{ID: id, Title: session.Title(), CreatedAt: session.CreatedAt, MessageCount: len(session.Messages)}) + } + + } + + return meta, nil +} + +// Delete removes a session from the SessionStore cache and any +// backing session file from disk. +func (ss *SessionStore) Delete(id string) error { + ss.mu.Lock() + defer ss.mu.Unlock() + + // return if no session found + session, ok := ss.cache[id] + if !ok { + return nil + } + // delete the backing file + err := os.Remove(session.FilePath) + if err != nil { + return err + } + // delete session from cache + delete(ss.cache, id) + return nil + +} + +// func createSessionMap(dir string) map[string]*Session { +// // Find all the jsonl files, parse them and add them to the map. +// // If a file fails to parse it should simply be logged as a warning +// m := make(map[string]*Session) +// os.DirFS() +// } + +func NewSessionStore(dir string) (*SessionStore, error) { + info, err := os.Stat(dir) + // Check if directory exists + if os.IsNotExist(err) { + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return nil, err + } + return &SessionStore{dir: dir, cache: make(map[string]*Session)}, nil + } + // Possible permission error + if err != nil { + return nil, err + } + + if !info.IsDir() { + // Go style guide says error messages shouldn't contain punctuation and be + // lowercase unless a proper noun. + return nil, fmt.Errorf("path %s is a file, not a directory", dir) + } + + //return &SessionStore{dir: dir, cache: createSessionMap(dir)}, nil + return &SessionStore{dir: dir, cache: make(map[string]*Session)}, nil +} + +func createSession(id, filepath string) (*Session, error) { + header := SessionHeader{ + Type: "session", + ID: id, + CreatedAt: time.Now().UTC(), + } + data, err := json.Marshal(header) + if err != nil { + return nil, err + } + // write the header to the file + if err := os.WriteFile(filepath, append(data, '\n'), 0o644); err != nil { + return nil, err + } + + return &Session{ + ID: id, + CreatedAt: header.CreatedAt, + FilePath: filepath, + Messages: []*schema.Message{}, + }, nil +} + +func loadSession(filePath string) (*Session, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + + // check for header + if !scanner.Scan() { + return nil, fmt.Errorf("empty session file: %s", filePath) + } + + var header SessionHeader + if err := json.Unmarshal(scanner.Bytes(), &header); err != nil { + return nil, fmt.Errorf("bad session header in %s: %w", filePath, err) + } + + session := &Session{ + ID: header.ID, + CreatedAt: header.CreatedAt, + FilePath: filePath, + Messages: make([]*schema.Message, 0), + } + + // populate messages + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + var msg schema.Message + if err := json.Unmarshal([]byte(line), &msg); err != nil { + //skip bad lines + continue + } + session.Messages = append(session.Messages, &msg) + } + + return session, nil +} diff --git a/eino/example/chapter3/store_test.go b/eino/example/chapter3/store_test.go new file mode 100644 index 0000000..89d5fe8 --- /dev/null +++ b/eino/example/chapter3/store_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cloudwego/eino/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testTime = time.Date(2026, time.May, 23, 12, 0, 0, 0, time.UTC) + +// testFile creates an session jsonl file with the appropriate SessionHeader starting +// line. +func testFile(t *testing.T, dir string) (string, string) { + t.Helper() + f, err := os.CreateTemp(dir, "test-session-*.jsonl") + if err != nil { + // Panic risk in call to f.Name() if in error because f would be nil + // t.Fatalf("failed to create temp session file %s", f.Name()) + t.Fatal("failed to create temp session file") + } + + fullPath := f.Name() + // Ensure files are cleaned up + t.Cleanup(func() { + _ = os.Remove(fullPath) + }) + + // os.File name can contain either the full path or the base file name + // depending on what the Open method arguments are. The use of filepath.Base + // ensures we only take the file name. + id := strings.TrimSuffix(filepath.Base(fullPath), ".jsonl") + + s := SessionHeader{Type: "session", ID: id, CreatedAt: testTime} + hData, err := json.Marshal(s) + if err != nil { + t.Fatalf("failed to marshal header data for %s", fullPath) + } + + hData = append(hData, '\n') + _, err = f.Write(hData) + if err != nil { + f.Close() + t.Fatalf("failed to write session header to %s", fullPath) + } + + if err = f.Close(); err != nil { + t.Fatalf("failed to close test file %s", fullPath) + } + + return fullPath, id +} + +func TestSessionAppend(t *testing.T) { + td := t.TempDir() + fp, id := testFile(t, td) + want := "Tell me about eino" + + s := Session{ID: id, CreatedAt: testTime, FilePath: fp} + + s.Append(&schema.Message{Role: schema.User, Content: want}) + + var msg *schema.Message + data, err := os.ReadFile(fp) + assert.NoError(t, err) + content := bytes.Split(data, []byte("\n")) + rawMsg := bytes.TrimSpace(content[1]) // possible out of index if Append failed + json.Unmarshal(rawMsg, &msg) // didn't check parse error + // we should convert to require which better documents the intention of the + // code and the test + assert.Equal(t, msg.Content, s.Messages[0].Content) + +} + +func TestSessionAppendBetter(t *testing.T) { + td := os.TempDir() + fp, id := testFile(t, td) + want := "Tell me about eino" + s := Session{ID: id, CreatedAt: testTime, FilePath: fp} + + err := s.Append(&schema.Message{Role: schema.User, Content: want}) + require.NoError(t, err, "s.Append should not return an error") + + data, err := os.ReadFile(s.FilePath) + require.NoError(t, err, "failed to read from session file") + // remove possible trailing newline in file contents + data = bytes.TrimSpace(data) + content := bytes.Split(data, []byte("\n")) + require.Len(t, content, 2, "files should contain two lines") + + var msg *schema.Message + err = json.Unmarshal(content[1], &msg) + require.NoError(t, err, "content should json parse as eino Message") + + require.Equal(t, want, msg.Content, "retrieved message content should match test content") + require.Len(t, s.Messages, 1, "session message should have 1 item") + require.Equal(t, want, s.Messages[0].Content, "in memory content should match test content") + +}