diff --git a/agent/dify/go.mod b/agent/dify/go.mod index b8eb49fca..ebf2a0b9c 100644 --- a/agent/dify/go.mod +++ b/agent/dify/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/agent/dify -go 1.21 +go 1.21.0 replace trpc.group/trpc-go/trpc-agent-go => ../.. diff --git a/examples/email/README.md b/examples/email/README.md new file mode 100644 index 000000000..be684cada --- /dev/null +++ b/examples/email/README.md @@ -0,0 +1,70 @@ +# email Example + +This example demonstrates how to handle various types of email (qq,gmail,163) using OpenAI-compatible models. + +## Features + +- **send email**: send email from your_email to other email, you can send whatever you want, but not support attachment + +## Quick Start + +```bash +# set your open base url, +export OPENAI_BASE_URL="https://api.openai.com/v1" +# Set your API key +export OPENAI_API_KEY="your-api-key-here" + +go run main.go -model gpt-4o-mini +``` + +## Example Session + +``` +🚀 Send Email Chat Demo +Model: gpt-5 +Streaming: true +Type 'exit' to end the conversation +Available tools: send_email +================================================== +✅ Email chat ready! Session: email-session-1763626040 +💡 Try asking questions like: + - send an email to zhuangguang5524621@gmail.com +👤 You: send an email to zhuangguang5524621@gmail.com +🤖 Assistant: I can send the email for you. Please provide the following so I can proceed: +- Email account credentials for sending: account name and password +- Subject line +- Email body/content +- Optional: the display “From” name you want to appear +Recipient to confirm: zhuangguang5524621@gmail.com +If you prefer, I can also draft the email content first and share it with you for approval before sending. +👤 You: name:zhuangguang5524621@gmail.com passwd: "xxxxxx" send an email to 1850396756@qq.com subject: hello content:
内容
+🤖 Assistant: +🔍 email initiated: + • email_send_email (ID: call_DxMS5B7zqSCj8jiEVx6pyG56) + Query: {"auth":{"name":"zhuangguang5524621@gmail.com","password":"xxxxx"},"mail_list":[{"to_email":"1850396756@qq.com","subject":"hello","content":"内容
"}]} +🔄 send email... +✅ send email results (ID: call_DxMS5B7zqSCj8jiEVx6pyG56): {"message":""} +Your email has been sent successfully. +Details: +- From: zhuangguang5524621@gmail.com +- To: 1850396756@qq.com +- Subject: hello +- Content (HTML): +内容
+If you’d like to send more emails or schedule one, let me know the details. +👤 You: exit +👋 Goodbye! +``` + +## How It Works + +1. **Setup**: The example creates an LLM agent with access to the email tool +2. **User Input**: Users can ask to send email +3. **Tool Detection**: The AI automatically decides when to use the email tool or ask more information of send email +4. **Email Send Execution**: The email tool performs send email and returns structured results +5. **Response Generation**: The AI uses the search results to provide informed, up-to-date responses + +## API Design & Limitations + +### Why These Limitations Exist +1. the send email tool use smtp protocol, mailbox have speed limit of send email. \ No newline at end of file diff --git a/examples/email/go.mod b/examples/email/go.mod new file mode 100644 index 000000000..69bfe711f --- /dev/null +++ b/examples/email/go.mod @@ -0,0 +1,49 @@ +module trpc.group/trpc-go/trpc-agent-go/examples/email + +go 1.24.0 + +replace ( + trpc.group/trpc-go/trpc-agent-go => ../.. + trpc.group/trpc-go/trpc-agent-go/tool/email => ../../tool/email +) + +require ( + trpc.group/trpc-go/trpc-agent-go v0.5.0 + trpc.group/trpc-go/trpc-agent-go/tool/email v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/openai/openai-go v1.12.0 // indirect + github.com/panjf2000/ants/v2 v2.10.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // 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/wneessen/go-mail v0.7.2 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect +) diff --git a/examples/email/go.sum b/examples/email/go.sum new file mode 100644 index 000000000..2072da155 --- /dev/null +++ b/examples/email/go.sum @@ -0,0 +1,95 @@ +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= +github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8= +github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= +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/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/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/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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/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= +github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= +github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +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= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/examples/email/main.go b/examples/email/main.go new file mode 100644 index 000000000..460d5a261 --- /dev/null +++ b/examples/email/main.go @@ -0,0 +1,261 @@ +package main + +import ( + "bufio" + "context" + "flag" + "fmt" + "log" + "os" + "strings" + "time" + + "trpc.group/trpc-go/trpc-agent-go/agent/llmagent" + "trpc.group/trpc-go/trpc-agent-go/event" + "trpc.group/trpc-go/trpc-agent-go/model" + "trpc.group/trpc-go/trpc-agent-go/model/openai" + "trpc.group/trpc-go/trpc-agent-go/runner" + "trpc.group/trpc-go/trpc-agent-go/tool" + "trpc.group/trpc-go/trpc-agent-go/tool/email" +) + +var ( + modelName = flag.String("model", "deepseek-chat", "Name of the model to use") +) + +func main() { + // Parse command line flags. + flag.Parse() + + fmt.Printf("🚀 Send Email Chat Demo\n") + fmt.Printf("Model: %s\n", *modelName) + fmt.Printf("Type 'exit' to end the conversation\n") + fmt.Printf("Available tools: send_email\n") + fmt.Println(strings.Repeat("=", 50)) + + // Create and run the chat. + chat := &emailChat{ + modelName: *modelName, + } + + if err := chat.run(); err != nil { + log.Fatalf("Chat failed: %s", err.Error()) + } +} + +type emailChat struct { + modelName string + runner runner.Runner + userID string + sessionID string + // current not support streaming + streaming bool +} + +// run starts the interactive chat session. +func (c *emailChat) run() error { + ctx := context.Background() + + // Setup the runner. + if err := c.setup(ctx); err != nil { + return fmt.Errorf("setup failed: %w", err) + } + + // Start interactive chat. + return c.startChat(ctx) +} + +// setup creates the runner with LLM agent and send email tool. +func (c *emailChat) setup(_ context.Context) error { + // Create OpenAI model. + modelInstance := openai.New(c.modelName) + + // Create email tool. + // For basic usage: + emailTool, err := email.NewToolSet() + if err != nil { + return fmt.Errorf("create file tool set: %w", err) + } + + // Create LLM agent with email tool. + genConfig := model.GenerationConfig{ + MaxTokens: intPtr(2000), + Temperature: floatPtr(0.7), + Stream: c.streaming, // Enable streaming + } + + agentName := "email-assistant" + llmAgent := llmagent.New( + agentName, + llmagent.WithModel(modelInstance), + llmagent.WithDescription("A helpful AI assistant with access to email sending capabilities"), + llmagent.WithInstruction("Use the email tool to send emails. ask user to provide account credentials. "+ + "if sending failed, error message contain web link, please tell the link to user"), + llmagent.WithGenerationConfig(genConfig), + llmagent.WithToolSets([]tool.ToolSet{emailTool}), + ) + + // Create runner. + appName := "email-agent" + c.runner = runner.NewRunner( + appName, + llmAgent, + ) + + // Setup identifiers. + c.userID = "user" + c.sessionID = fmt.Sprintf("email-session-%d", time.Now().Unix()) + + fmt.Printf("✅ Email chat ready! Session: %s\n\n", c.sessionID) + return nil +} + +// startChat runs the interactive conversation loop. +func (c *emailChat) startChat(ctx context.Context) error { + scanner := bufio.NewScanner(os.Stdin) + + // Print welcome message with examples. + fmt.Println("💡 Try asking questions like:") + fmt.Println(" - send an email to zhuangguang5524621@gmail.com user:your_email password:your_password subject:subject content:content") + fmt.Println() + + for { + fmt.Print("👤 You: ") + if !scanner.Scan() { + break + } + + userInput := strings.TrimSpace(scanner.Text()) + if userInput == "" { + continue + } + + // Handle exit command. + if strings.ToLower(userInput) == "exit" { + fmt.Println("👋 Goodbye!") + return nil + } + + // Process the user message. + if err := c.processMessage(ctx, userInput); err != nil { + fmt.Printf("❌ Error: %v\n", err) + } + + fmt.Println() // Add spacing between turns + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("input scanner error: %w", err) + } + + return nil +} + +// processMessage handles a single message exchange. +func (c *emailChat) processMessage(ctx context.Context, userMessage string) error { + message := model.NewUserMessage(userMessage) + // Run the agent through the runner. + eventChan, err := c.runner.Run(ctx, c.userID, c.sessionID, message) + if err != nil { + return fmt.Errorf("failed to run agent: %w", err) + } + // Process streaming response. + return c.processResponse(eventChan) +} + +// processResponse handles the response with email tool visualization. +func (c *emailChat) processResponse(eventChan <-chan *event.Event) error { + fmt.Print("🤖 Assistant: ") + + var ( + fullContent string + toolCallsDetected bool + assistantStarted bool + ) + + for event := range eventChan { + + // Handle errors. + if event.Error != nil { + fmt.Printf("\n❌ Error: %s\n", event.Error.Message) + continue + } + + // Detect and display tool calls. + if len(event.Response.Choices) > 0 && len(event.Response.Choices[0].Message.ToolCalls) > 0 { + toolCallsDetected = true + if assistantStarted { + fmt.Printf("\n") + } + fmt.Printf("🔍 email initiated:\n") + for _, toolCall := range event.Response.Choices[0].Message.ToolCalls { + fmt.Printf(" • %s (ID: %s)\n", toolCall.Function.Name, toolCall.ID) + if len(toolCall.Function.Arguments) > 0 { + fmt.Printf(" Query: %s\n", string(toolCall.Function.Arguments)) + } + } + fmt.Printf("\n🔄 send email...\n") + } + + // Detect tool responses. + if event.Response != nil && len(event.Response.Choices) > 0 { + hasToolResponse := false + for _, choice := range event.Response.Choices { + if choice.Message.Role == model.RoleTool && choice.Message.ToolID != "" { + fmt.Printf("✅ send email results (ID: %s): %s\n", + choice.Message.ToolID, + strings.TrimSpace(choice.Message.Content)) + hasToolResponse = true + } + } + if hasToolResponse { + continue + } + } + + // Process content from choices. + if len(event.Response.Choices) > 0 { + choice := event.Response.Choices[0] + + if !assistantStarted { + if toolCallsDetected { + fmt.Printf("\n🤖 Assistant: ") + } + assistantStarted = true + } + + // Handle content based on streaming mode. + var content string + if c.streaming { + // Streaming mode: use delta content. + content = choice.Delta.Content + } else { + // Non-streaming mode: use full message content. + content = choice.Message.Content + } + + if content != "" { + fmt.Print(content) + fullContent += content + } + } + + // Check if this is the final event. + if event.Done { + fmt.Printf("\n") + break + } + } + + return nil +} + +// intPtr returns a pointer to the given int. +func intPtr(i int) *int { + return &i +} + +// floatPtr returns a pointer to the given float64. +func floatPtr(f float64) *float64 { + return &f +} diff --git a/examples/go.mod b/examples/go.mod index ad9e00305..f8cdf604f 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,8 +1,27 @@ module trpc.group/trpc-go/trpc-agent-go/examples -go 1.23.0 +go 1.24.0 -replace trpc.group/trpc-go/trpc-agent-go => ../ +toolchain go1.24.10 + +replace ( + trpc.group/trpc-go/trpc-agent-go => ../ + trpc.group/trpc-go/trpc-agent-go/codeexecutor/container => ../codeexecutor/container + trpc.group/trpc-go/trpc-agent-go/codeexecutor/jupyter => ../codeexecutor/jupyter + trpc.group/trpc-go/trpc-agent-go/knowledge/vectorstore/pgvector => ../knowledge/vectorstore/pgvector + trpc.group/trpc-go/trpc-agent-go/knowledge/vectorstore/tcvector => ../knowledge/vectorstore/tcvector + trpc.group/trpc-go/trpc-agent-go/memory/mysql => ../memory/mysql + trpc.group/trpc-go/trpc-agent-go/memory/postgres => ../memory/postgres + trpc.group/trpc-go/trpc-agent-go/memory/redis => ../memory/redis + trpc.group/trpc-go/trpc-agent-go/session/postgres => ../session/postgres + trpc.group/trpc-go/trpc-agent-go/session/redis => ../session/redis/ + trpc.group/trpc-go/trpc-agent-go/storage/mysql => ../storage/mysql + trpc.group/trpc-go/trpc-agent-go/storage/postgres => ../storage/postgres + trpc.group/trpc-go/trpc-agent-go/storage/redis => ../storage/redis + trpc.group/trpc-go/trpc-agent-go/storage/tcvector => ../storage/tcvector + trpc.group/trpc-go/trpc-agent-go/tool/arxivsearch => ../tool/arxivsearch + trpc.group/trpc-go/trpc-agent-go/tool/google => ../tool/google +) require ( github.com/google/uuid v1.6.0 @@ -12,8 +31,8 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/zap v1.27.0 - trpc.group/trpc-go/trpc-a2a-go v0.2.5 - trpc.group/trpc-go/trpc-agent-go v0.5.0 + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a + trpc.group/trpc-go/trpc-agent-go v0.4.0 trpc.group/trpc-go/trpc-mcp-go v0.0.10 ) @@ -49,6 +68,7 @@ require ( 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/wneessen/go-mail v0.7.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect @@ -61,13 +81,13 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/examples/go.sum b/examples/go.sum index d2474a4aa..515cc31f9 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -96,6 +96,8 @@ 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/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= +github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -130,21 +132,21 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= @@ -158,7 +160,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/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= -trpc.group/trpc-go/trpc-a2a-go v0.2.5 h1:X3pAlWD128LaS9TtXsUDZoJWPVuPZDkZKUecKRxmWn4= -trpc.group/trpc-go/trpc-a2a-go v0.2.5/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= trpc.group/trpc-go/trpc-mcp-go v0.0.10 h1:kKPfevmikMojfOgtUBf5SJQ/v6aDugckodgyH1uDu2Q= trpc.group/trpc-go/trpc-mcp-go v0.0.10/go.mod h1:OT6rLglkdaQ17D2T1Y87Y/ckItzdsEldDbw7dHAbGEA= diff --git a/go.mod b/go.mod index 5d73fcf21..687eefddb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go -go 1.21 +go 1.21.0 require ( github.com/bmatcuk/doublestar/v4 v4.9.1 diff --git a/knowledge/embedder/huggingface/go.mod b/knowledge/embedder/huggingface/go.mod index 62bae699c..c8b347e7e 100644 --- a/knowledge/embedder/huggingface/go.mod +++ b/knowledge/embedder/huggingface/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/knowledge/embedder/huggingface -go 1.21 +go 1.21.0 replace trpc.group/trpc-go/trpc-agent-go => ../../.. diff --git a/knowledge/vectorstore/pgvector/go.mod b/knowledge/vectorstore/pgvector/go.mod index 737b8ad4c..d4170c2b8 100644 --- a/knowledge/vectorstore/pgvector/go.mod +++ b/knowledge/vectorstore/pgvector/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/knowledge/vectorstore/pgvector -go 1.21 +go 1.21.0 replace ( trpc.group/trpc-go/trpc-agent-go => ../../../ diff --git a/knowledge/vectorstore/tcvector/go.mod b/knowledge/vectorstore/tcvector/go.mod index 65b77c35c..1720db3ef 100644 --- a/knowledge/vectorstore/tcvector/go.mod +++ b/knowledge/vectorstore/tcvector/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/knowledge/vectorstore/tcvector -go 1.21 +go 1.21.0 replace ( trpc.group/trpc-go/trpc-agent-go => ../../../ diff --git a/memory/postgres/go.mod b/memory/postgres/go.mod index ba9e7a108..4f63efc4d 100644 --- a/memory/postgres/go.mod +++ b/memory/postgres/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/memory/postgres -go 1.21 +go 1.21.0 replace ( trpc.group/trpc-go/trpc-agent-go => ../.. diff --git a/memory/redis/go.mod b/memory/redis/go.mod index bce2ddc5e..bd722a3db 100644 --- a/memory/redis/go.mod +++ b/memory/redis/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/memory/redis -go 1.21 +go 1.21.0 replace ( trpc.group/trpc-go/trpc-agent-go => ../../ diff --git a/session/postgres/go.mod b/session/postgres/go.mod index eff04d131..2afce0c9c 100644 --- a/session/postgres/go.mod +++ b/session/postgres/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/session/postgres -go 1.21 +go 1.21.0 replace ( trpc.group/trpc-go/trpc-agent-go => ../../ diff --git a/session/redis/go.mod b/session/redis/go.mod index b3ad0dcc5..addeab2e2 100644 --- a/session/redis/go.mod +++ b/session/redis/go.mod @@ -1,6 +1,6 @@ module trpc.group/trpc-go/trpc-agent-go/session/redis -go 1.21 +go 1.21.0 replace ( trpc.group/trpc-go/trpc-agent-go => ../../ diff --git a/tool/email/email.go b/tool/email/email.go new file mode 100644 index 000000000..c5ba0da7a --- /dev/null +++ b/tool/email/email.go @@ -0,0 +1,101 @@ +// +// Tencent is pleased to support the open source community by making trpc-agent-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-agent-go is licensed under the Apache License Version 2.0. +// +// + +// Package email provides send email tools for AI agents. +// This tool can send emails to personal email and some Corporate Email. +package email + +import ( + "context" + + "trpc.group/trpc-go/trpc-agent-go/tool" +) + +const ( + // default name + defaultName = "email" +) + +// MailboxType mailbox +type MailboxType int32 + +const ( + // MAIL_UNKNOWN unknown mail + MAIL_UNKNOWN MailboxType = 0 + // MAIL_QQ qq mail + MAIL_QQ MailboxType = 1 + // MAIL_163 163 mail + MAIL_163 MailboxType = 2 + // MAIL_GMAIL google mail + MAIL_GMAIL MailboxType = 3 +) + +// MailboxTypeToString convert mailbox type to string +func MailboxTypeToString(mailboxType MailboxType) string { + switch mailboxType { + // qq mail + case MAIL_QQ: + return "qq" + // 163 mail + case MAIL_163: + return "163" + // google mail + case MAIL_GMAIL: + return "gmail" + // unknown mail + default: + return "unknown" + } +} + +// Option is a functional option for configuring the file tool set. +type Option func(*emailToolSet) + +// emailToolSet implements the ToolSet interface for file operations. +type emailToolSet struct { + sendEmailEnabled bool + tools []tool.Tool +} + +// Tools implements the ToolSet interface. +func (e *emailToolSet) Tools(_ context.Context) []tool.Tool { + return e.tools +} + +// Name implements the ToolSet interface. +func (e *emailToolSet) Name() string { + return "email" +} + +// Close implements the ToolSet interface. +func (e *emailToolSet) Close() error { + // No resources to clean up for file tools. + return nil +} + +// NewToolSet creates a new file tool set with the given options. +func NewToolSet(opts ...Option) (tool.ToolSet, error) { + emailToolSet := &emailToolSet{ + sendEmailEnabled: true, + tools: nil, + } + + // Apply user-provided options. + for _, opt := range opts { + opt(emailToolSet) + } + + // Create function tools based on enabled features. + var tools []tool.Tool + if emailToolSet.sendEmailEnabled { + tools = append(tools, emailToolSet.sendMailTool()) + } + emailToolSet.tools = tools + return emailToolSet, nil +} diff --git a/tool/email/email_test.go b/tool/email/email_test.go new file mode 100644 index 000000000..cf27c1dc0 --- /dev/null +++ b/tool/email/email_test.go @@ -0,0 +1,124 @@ +package email + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "trpc.group/trpc-go/trpc-agent-go/tool" +) + +func TestNewToolSet_Default(t *testing.T) { + set, err := NewToolSet() + assert.NoError(t, err) + ets := set.(*emailToolSet) + assert.Equal(t, true, ets.sendEmailEnabled) +} + +// 由CodeBuddy(内网版)生成于2025.11.27 09:52:42 +func Test_emailToolSet_Close(t *testing.T) { + type fields struct { + sendEmailEnabled bool + tools []tool.Tool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "zero value", + fields: fields{}, + wantErr: false, + }, + { + name: "enabled with tools", + fields: fields{ + sendEmailEnabled: true, + tools: []tool.Tool{}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &emailToolSet{ + sendEmailEnabled: tt.fields.sendEmailEnabled, + tools: tt.fields.tools, + } + err := e.Close() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +// 由CodeBuddy(内网版)生成于2025.11.27 09:52:43 +func Test_emailToolSet_Name(t *testing.T) { + tests := []struct { + name string + fields struct { + sendEmailEnabled bool + tools []tool.Tool + } + want string + }{ + { + name: "default zero value", + fields: struct { + sendEmailEnabled bool + tools []tool.Tool + }{}, + want: "email", + }, + { + name: "non-zero fields", + fields: struct { + sendEmailEnabled bool + tools []tool.Tool + }{ + sendEmailEnabled: true, + tools: []tool.Tool{}, + }, + want: "email", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &emailToolSet{ + sendEmailEnabled: tt.fields.sendEmailEnabled, + tools: tt.fields.tools, + } + assert.Equal(t, tt.want, e.Name()) + }) + } +} + +// 由CodeBuddy(内网版)生成于2025.11.27 09:52:44 +func TestMailboxTypeToString(t *testing.T) { + type args struct { + mailboxType MailboxType + } + tests := []struct { + name string + args args + want string + }{ + {"qq", args{mailboxType: MAIL_QQ}, "qq"}, + {"163", args{mailboxType: MAIL_163}, "163"}, + {"gmail", args{mailboxType: MAIL_GMAIL}, "gmail"}, + {"zero", args{mailboxType: 0}, "unknown"}, + {"negative", args{mailboxType: -1}, "unknown"}, + {"undefined", args{mailboxType: 99}, "unknown"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MailboxTypeToString(tt.args.mailboxType); got != tt.want { + t.Errorf("MailboxTypeToString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/tool/email/go.mod b/tool/email/go.mod new file mode 100644 index 000000000..a043a0def --- /dev/null +++ b/tool/email/go.mod @@ -0,0 +1,21 @@ +module trpc.group/trpc-go/trpc-agent-go/tool/email + +go 1.24.0 + +replace trpc.group/trpc-go/trpc-agent-go => ../.. + +require ( + github.com/stretchr/testify v1.11.1 + github.com/wneessen/go-mail v0.7.2 + trpc.group/trpc-go/trpc-agent-go v0.5.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/text v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect +) diff --git a/tool/email/go.sum b/tool/email/go.sum new file mode 100644 index 000000000..cdf7d1e06 --- /dev/null +++ b/tool/email/go.sum @@ -0,0 +1,22 @@ +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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= +github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +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= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/tool/email/sendmail.go b/tool/email/sendmail.go new file mode 100644 index 000000000..71df409bc --- /dev/null +++ b/tool/email/sendmail.go @@ -0,0 +1,219 @@ +package email + +import ( + "context" + "errors" + "fmt" + "strings" + + "trpc.group/trpc-go/trpc-agent-go/log" + + gomail "github.com/wneessen/go-mail" + "trpc.group/trpc-go/trpc-agent-go/tool" + "trpc.group/trpc-go/trpc-agent-go/tool/function" + + "net/mail" +) + +const ( + qqMail = "smtp.qq.com" + qqPort = 465 + gmailMail = "smtp.gmail.com" + gmailPort = 587 + netEase163Mail = "smtp.163.com" + netEase1163Port = 465 +) + +// sendMailRequest represents the input for the send mail operation. +type sendMailRequest struct { + Auth Auth `json:"auth" jsonschema:"description=auth of the mail."` + MailList []*Mail `json:"mail_list" jsonschema:"description=The list of mail."` + Extra ExtraData `json:"extra" jsonschema:"description=extra data of the mail. optional. default is empty."` +} + +type Mail struct { + ToEmail string `json:"to_email" jsonschema:"description=send to email."` + Subject string `json:"subject" jsonschema:"description=subject of the mail"` + Content string `json:"content" jsonschema:"description=content of the mail"` +} + +// Auth is a struct for email authentication. +type Auth struct { + Name string `json:"name" jsonschema:"description=name of the mail."` + Password string `json:"password" jsonschema:"description=password of the mail."` +} + +type ExtraData struct { + SvrAddr string `json:"svr_addr" jsonschema:"description=server address of the mail. optional. default is empty."` + Port int `json:"port" jsonschema:"description=port of the mail. optional. default is empty."` +} + +// sendMailResponse represents the output from the send mail operation. +type sendMailResponse struct { + Message string `json:"message"` +} + +// sendMail performs the send mail operation. +func (e *emailToolSet) sendMail(ctx context.Context, req *sendMailRequest) (rsp *sendMailResponse, err error) { + rsp = &sendMailResponse{} + + addr, port, isSSL, err := e.getEmailAddr(req) + if err != nil { + rsp.Message = fmt.Sprintf("getSvrAddrAndPort ERROR: %v", err) + return + } + + opts := []gomail.Option{ + gomail.WithPort(port), + gomail.WithSMTPAuth(gomail.SMTPAuthAutoDiscover), + gomail.WithUsername(req.Auth.Name), + gomail.WithPassword(req.Auth.Password), + gomail.WithoutNoop(), + gomail.WithDebugLog(), + } + if isSSL { + opts = append(opts, gomail.WithSSL()) + } else { + opts = append(opts, gomail.WithTLSPolicy(gomail.TLSMandatory)) + } + + client, err := gomail.NewClient( + addr, + opts..., + ) + if err != nil { + rsp.Message = fmt.Sprintf("the server address err: %v", err) + return rsp, nil + } + + defer func() { + _ = client.Close() + }() + + messages := make([]*gomail.Msg, 0, len(req.MailList)) + for _, m := range req.MailList { + message := gomail.NewMsg() + err = message.From(req.Auth.Name) + if err != nil { + rsp.Message = fmt.Sprintf("from email err: %v", err) + return rsp, nil + } + err = message.To(m.ToEmail) + if err != nil { + rsp.Message = fmt.Sprintf("to email err: %v", err) + return rsp, nil + } + message.Subject(m.Subject) + message.SetBodyString(gomail.TypeTextHTML, m.Content) + messages = append(messages, message) + } + + // batch send email, not stop if one failed, return err which join all send error message + if err := client.DialAndSendWithContext(ctx, messages...); err != nil { + //qq mail special error handle + //https://github.com/wneessen/go-mail/issues/463 + if addr == qqMail && qqHandleError(err) == nil { + + } else { + rsp.Message = fmt.Sprintf("send ERROR: %v, host:%s, port:%d", err, addr, port) + return rsp, nil + } + } + + return +} + +// getEmailAddr gets the email address and port. +func (e *emailToolSet) getEmailAddr(req *sendMailRequest) (addr string, port int, isSSL bool, err error) { + mailBoxType, err := checkMailBoxType(req.Auth.Name) + if err != nil { + err = fmt.Errorf("checkMailBoxType ERROR: %v", err) + return + } + + if req.Extra.SvrAddr != "" { + addr = req.Extra.SvrAddr + port = req.Extra.Port + } else { + switch mailBoxType { + case MAIL_QQ: + //qq email + addr = qqMail + port = qqPort + isSSL = true + case MAIL_GMAIL: + //gmail email + addr = gmailMail + port = gmailPort + isSSL = false + case MAIL_163: + //163 email + addr = netEase163Mail + port = netEase1163Port + isSSL = true + default: + // not support + err = fmt.Errorf("not support mailbox type:%s", MailboxTypeToString(mailBoxType)) + return + } + } + return +} + +// checkMailBoxType checks the mailbox type. +func checkMailBoxType(email string) (MailboxType, error) { + + addr, err := mail.ParseAddress(email) + if err != nil { + return MAIL_UNKNOWN, fmt.Errorf("parse email address ERROR: %w", err) + } + log.Infof("addr: %v", addr) + // to lower + emailAddr := strings.ToLower(addr.Address) + log.Infof("emailAddr: %v", emailAddr) + + // split by name and domain + lastAt := strings.LastIndex(emailAddr, "@") + if lastAt < 0 { + return MAIL_UNKNOWN, fmt.Errorf("invalid email address") + } + domain := emailAddr[lastAt:] + domain = strings.TrimPrefix(domain, "@") + log.Infof("domain: %v", domain) + + switch domain { + case "qq.com", "vip.qq.com", "foxmail.com": + return MAIL_QQ, nil + case "gmail.com", "googlemail.com": + return MAIL_GMAIL, nil + case "163.com": + return MAIL_163, nil + default: + return MAIL_UNKNOWN, nil + } +} + +// sendMailTool returns a callable tool for send mail. +func (e *emailToolSet) sendMailTool() tool.CallableTool { + return function.NewFunctionTool( + e.sendMail, + function.WithName("send_email"), + function.WithDescription("send mail to other"), + ) +} + +func qqHandleError(err error) error { + log.Infof("err: %v %T", err, err) + + var sendErr *gomail.SendError + // Check if this is an SMTP RESET error after successful delivery + if errors.As(err, &sendErr) { + if sendErr.Reason == gomail.ErrSMTPReset { + // https://github.com/wneessen/go-mail/issues/463 + log.Warnf("⚠️ Mail delivered successfully but SMTP RESET failed: %s", err) + return nil // Don't treat this as a delivery failure since mail was sent + } + return err + } + return err +} diff --git a/tool/email/sendmail_test.go b/tool/email/sendmail_test.go new file mode 100644 index 000000000..ec7bfc653 --- /dev/null +++ b/tool/email/sendmail_test.go @@ -0,0 +1,391 @@ +package email + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_emailToolSet_sendMail(t *testing.T) { + toolSet, err := NewToolSet() + if err != nil { + t.Errorf("NewToolSet failed, err: %v", err) + } + + tests := []struct { + Name string + Password string + ToEmail string + Subject string + Content string + wantErr bool + }{ + // qq to gmail + { + Name: "1850396756@qq.com", + Password: "", + ToEmail: "zhuangguang5524621@gmail.com", + Subject: "test", + Content: "test", + wantErr: false, + }, + // gmail to qq + { + Name: "zhuangguang5524621@gmail.com", + Password: "", + ToEmail: "1850396756@qq.com", + Subject: "test", + Content: "test", + wantErr: false, + }, + // 163 to gmail + { + Name: "18218025138@163.com", + Password: "", + ToEmail: "zhuangguang5524621@gmail.com", + Subject: "test", + Content: "test", + wantErr: false, + }, + } + for _, tt := range tests { + + rsp, err := toolSet.(*emailToolSet).sendMail(context.Background(), &sendMailRequest{ + Auth: Auth{ + Name: tt.Name, + Password: tt.Password, + }, + MailList: []*Mail{ + { + ToEmail: tt.ToEmail, + Subject: tt.Subject, + Content: tt.Content, + }, + }, + }) + t.Logf("rsp: %+v err:%v", rsp, err) + if tt.Password == "" { + t.Skip("password is empty, skip") + } + if rsp.Message != "" { + if tt.wantErr == false { + t.Errorf("send mail err: %s", rsp.Message) + } + } else if tt.wantErr == true { + t.Errorf("should err but not") + } + + } +} + +func Test_emailToolSet_sendMail2(t *testing.T) { + toolSet, err := NewToolSet() + if err != nil { + t.Errorf("NewToolSet failed, err: %v", err) + } + + tests := []struct { + Name string + Password string + ToEmail string + Subject string + Content string + wantErr bool + }{ + // error case + { + Name: "18503@96756@qq.com", + Password: "", + ToEmail: "zhuangguang5524621@gmail.com", + Subject: "test", + Content: "test", + wantErr: true, + }, + // error case + { + Name: "zhuangguang5524621@gmail.com", + Password: "", + ToEmail: "185039@6756@qq.com", + Subject: "test", + Content: "test", + wantErr: true, + }, + } + for _, tt := range tests { + + rsp, err := toolSet.(*emailToolSet).sendMail(context.Background(), &sendMailRequest{ + Auth: Auth{ + Name: tt.Name, + Password: tt.Password, + }, + MailList: []*Mail{ + { + ToEmail: tt.ToEmail, + Subject: tt.Subject, + Content: tt.Content, + }, + }, + }) + t.Logf("rsp: %+v err:%v", rsp, err) + if rsp.Message != "" { + if tt.wantErr == false { + t.Errorf("send mail err: %s", rsp.Message) + } + } else if tt.wantErr == true { + t.Errorf("should err but not") + } + + } +} + +func Test_checkMailBoxType(t *testing.T) { + type args struct { + email string + } + tests := []struct { + name string + args args + want MailboxType + wantErr bool + }{ + { + name: "QQ domain", + args: args{email: "user@qq.com"}, + want: MAIL_QQ, + wantErr: false, + }, + { + name: "QQ vip domain", + args: args{email: "user@vip.qq.com"}, + want: MAIL_QQ, + wantErr: false, + }, + { + name: "Foxmail domain", + args: args{email: "user@foxmail.com"}, + want: MAIL_QQ, + wantErr: false, + }, + { + name: "Gmail domain", + args: args{email: "user@gmail.com"}, + want: MAIL_GMAIL, + wantErr: false, + }, + { + name: "Googlemail domain", + args: args{email: "user@googlemail.com"}, + want: MAIL_GMAIL, + wantErr: false, + }, + { + name: "163 domain", + args: args{email: "user@163.com"}, + want: MAIL_163, + wantErr: false, + }, + { + name: "Unknown domain", + args: args{email: "user@example.com"}, + want: MAIL_UNKNOWN, + wantErr: false, + }, + { + name: "Empty email", + args: args{email: ""}, + want: MAIL_UNKNOWN, + wantErr: true, + }, + { + name: "No @ symbol", + args: args{email: "invalid-email"}, + want: MAIL_UNKNOWN, + wantErr: true, + }, + { + name: "Multiple @ symbols", + args: args{email: "user@@example.com"}, + want: MAIL_UNKNOWN, + wantErr: true, + }, + { + name: "Uppercase QQ domain", + args: args{email: "USER@QQ.COM"}, + want: MAIL_QQ, + wantErr: false, + }, + { + name: "Mixed case Gmail", + args: args{email: "User@GmAiL.cOm"}, + want: MAIL_GMAIL, + wantErr: false, + }, + { + name: "not valid email", + args: args{email: "UserGmAiL.cOm"}, + want: MAIL_UNKNOWN, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := checkMailBoxType(tt.args.email) + if (err != nil) != tt.wantErr { + t.Errorf("checkMailBoxType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("checkMailBoxType() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_emailToolSet_sendMailTool(t *testing.T) { + e := &emailToolSet{} + + got := e.sendMailTool() + assert.NotNil(t, got) + + decl := got.Declaration() + assert.Equal(t, "send_email", decl.Name) + assert.Equal(t, "send mail to other", decl.Description) +} + +func Test_emailToolSet_getEmailAddr(t *testing.T) { + type args struct { + req *sendMailRequest + } + tests := []struct { + name string + e *emailToolSet + args args + wantAddr string + wantPort int + wantIsSSL bool + wantErr bool + }{ + { + name: "custom server", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "user@example.com"}, + Extra: ExtraData{ + SvrAddr: "smtp.example.com", + Port: 2525, + }, + }, + }, + wantAddr: "smtp.example.com", + wantPort: 2525, + wantIsSSL: false, + wantErr: false, + }, + { + name: "qq mail", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "123@qq.com"}, + }, + }, + wantAddr: qqMail, + wantPort: qqPort, + wantIsSSL: true, + wantErr: false, + }, + { + name: "gmail", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "abc@gmail.com"}, + }, + }, + wantAddr: gmailMail, + wantPort: gmailPort, + wantIsSSL: false, + wantErr: false, + }, + { + name: "163 mail", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "user@163.com"}, + }, + }, + wantAddr: netEase163Mail, + wantPort: netEase1163Port, + wantIsSSL: true, + wantErr: false, + }, + { + name: "invalid email format", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "not-an-email"}, + }, + }, + wantErr: true, + }, + { + name: "unsupported domain", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "user@icloud.com"}, + }, + }, + wantErr: true, + }, + { + name: "empty auth name", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: ""}, + }, + }, + wantErr: true, + }, + { + name: "zero port with custom server", + e: &emailToolSet{}, + args: args{ + req: &sendMailRequest{ + Auth: Auth{Name: "user@example.com"}, + Extra: ExtraData{ + SvrAddr: "smtp.example.com", + Port: 0, + }, + }, + }, + wantAddr: "smtp.example.com", + wantPort: 0, + wantIsSSL: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAddr, gotPort, gotIsSSL, err := tt.e.getEmailAddr(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("getEmailAddr() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotAddr != tt.wantAddr { + t.Errorf("getEmailAddr() gotAddr = %v, want %v", gotAddr, tt.wantAddr) + } + if gotPort != tt.wantPort { + t.Errorf("getEmailAddr() gotPort = %v, want %v", gotPort, tt.wantPort) + } + if gotIsSSL != tt.wantIsSSL { + t.Errorf("getEmailAddr() gotIsSSL = %v, want %v", gotIsSSL, tt.wantIsSSL) + } + }) + } +}