Skip to content

Commit f8fd4ad

Browse files
committed
feat(tool):implement email toolSet(send mail) and add example to use
1 parent 852c8dd commit f8fd4ad

File tree

10 files changed

+710
-0
lines changed

10 files changed

+710
-0
lines changed

examples/email/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# email Example
2+
3+
This example demonstrates how to handle various types of email (qq,gmail) using OpenAI-compatible models.
4+
5+
## Features
6+
7+
- **send email**: send email from your_email to other email, you can send whatever you want, but not support attachment
8+
9+
## Quick Start
10+
11+
```bash
12+
# set your open base url,
13+
export OPENAI_BASE_URL="http://ieval.woa.com/openapi/v1"
14+
# Set your API key
15+
export OPENAI_API_KEY="your-api-key-here"
16+
17+
go run main.go -model gpt-4o-mini
18+
```
19+
20+
## Example Session
21+
22+
```
23+
🚀 Send Email Chat Demo
24+
Model: gpt-5
25+
Streaming: true
26+
Type 'exit' to end the conversation
27+
Available tools: send_email
28+
==================================================
29+
✅ Email chat ready! Session: email-session-1763626040
30+
💡 Try asking questions like:
31+
- send an email to [email protected]
32+
👤 You: send an email to [email protected]
33+
🤖 Assistant: I can send the email for you. Please provide the following so I can proceed:
34+
- Email account credentials for sending: account name and password
35+
- Subject line
36+
- Email body/content
37+
- Optional: the display “From” name you want to appear
38+
Recipient to confirm: [email protected]
39+
If you prefer, I can also draft the email content first and share it with you for approval before sending.
40+
👤 You: name:[email protected] passwd: "xxxxxx" send an email to [email protected] subject: hello content:<html><body><h1>标题</h1><p>内容</p></body></html>
41+
🤖 Assistant:
42+
🔍 email initiated:
43+
• email_send_email (ID: call_DxMS5B7zqSCj8jiEVx6pyG56)
44+
Query: {"auth":{"name":"[email protected]","password":"xxxxx"},"mail_list":[{"to_email":"[email protected]","subject":"hello","content":"<html><body><h1>标题</h1><p>内容</p></body></html>"}]}
45+
🔄 send email...
46+
✅ send email results (ID: call_DxMS5B7zqSCj8jiEVx6pyG56): {"message":""}
47+
Your email has been sent successfully.
48+
Details:
49+
50+
51+
- Subject: hello
52+
- Content (HTML):
53+
<html><body><h1>标题</h1><p>内容</p></body></html>
54+
If you’d like to send more emails or schedule one, let me know the details.
55+
👤 You: exit
56+
👋 Goodbye!
57+
```
58+
59+
## How It Works
60+
61+
1. **Setup**: The example creates an LLM agent with access to the email tool
62+
2. **User Input**: Users can ask to send email
63+
3. **Tool Detection**: The AI automatically decides when to use the email tool or ask more information of send email
64+
4. **Email Send Execution**: The email tool performs send email and returns structured results
65+
5. **Response Generation**: The AI uses the search results to provide informed, up-to-date responses
66+
67+
## API Design & Limitations
68+
69+
### Why These Limitations Exist
70+
1. the send email tool use smtp protocol, mailbox have speed limit of send email.

examples/email/main.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"os"
10+
"strings"
11+
"time"
12+
"trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
13+
"trpc.group/trpc-go/trpc-agent-go/event"
14+
"trpc.group/trpc-go/trpc-agent-go/model"
15+
"trpc.group/trpc-go/trpc-agent-go/model/openai"
16+
"trpc.group/trpc-go/trpc-agent-go/runner"
17+
"trpc.group/trpc-go/trpc-agent-go/tool"
18+
"trpc.group/trpc-go/trpc-agent-go/tool/email"
19+
)
20+
21+
var (
22+
streaming = flag.Bool("streaming", true, "Enable streaming mode for responses")
23+
modelName = flag.String("model", "deepseek-chat", "Name of the model to use")
24+
)
25+
26+
func main() {
27+
// Parse command line flags.
28+
flag.Parse()
29+
30+
fmt.Printf("🚀 Send Email Chat Demo\n")
31+
fmt.Printf("Model: %s\n", *modelName)
32+
fmt.Printf("Streaming: %t\n", *streaming)
33+
fmt.Printf("Type 'exit' to end the conversation\n")
34+
fmt.Printf("Available tools: send_email\n")
35+
fmt.Println(strings.Repeat("=", 50))
36+
37+
// Create and run the chat.
38+
chat := &emailChat{
39+
modelName: *modelName,
40+
streaming: *streaming,
41+
}
42+
43+
if err := chat.run(); err != nil {
44+
log.Fatal("Chat failed: %v", err)
45+
}
46+
}
47+
48+
type emailChat struct {
49+
modelName string
50+
runner runner.Runner
51+
userID string
52+
sessionID string
53+
streaming bool
54+
}
55+
56+
// run starts the interactive chat session.
57+
func (c *emailChat) run() error {
58+
ctx := context.Background()
59+
60+
// Setup the runner.
61+
if err := c.setup(ctx); err != nil {
62+
return fmt.Errorf("setup failed: %w", err)
63+
}
64+
65+
// Start interactive chat.
66+
return c.startChat(ctx)
67+
}
68+
69+
// setup creates the runner with LLM agent and send email tool.
70+
func (c *emailChat) setup(ctx context.Context) error {
71+
// Create OpenAI model.
72+
modelInstance := openai.New(c.modelName)
73+
74+
// Create email tool.
75+
// For basic usage:
76+
emailTool, err := email.NewToolSet()
77+
if err != nil {
78+
return fmt.Errorf("create file tool set: %w", err)
79+
}
80+
81+
// Create LLM agent with email tool.
82+
genConfig := model.GenerationConfig{
83+
MaxTokens: intPtr(2000),
84+
Temperature: floatPtr(0.7),
85+
Stream: c.streaming, // Enable streaming
86+
}
87+
88+
agentName := "email-assistant"
89+
llmAgent := llmagent.New(
90+
agentName,
91+
llmagent.WithModel(modelInstance),
92+
llmagent.WithDescription("A helpful AI assistant with access to email sending capabilities"),
93+
llmagent.WithInstruction("Use the email tool to send emails. ask user to provide account credentials"),
94+
llmagent.WithGenerationConfig(genConfig),
95+
llmagent.WithToolSets([]tool.ToolSet{emailTool}),
96+
)
97+
98+
// Create runner.
99+
appName := "email-agent"
100+
c.runner = runner.NewRunner(
101+
appName,
102+
llmAgent,
103+
)
104+
105+
// Setup identifiers.
106+
c.userID = "user"
107+
c.sessionID = fmt.Sprintf("email-session-%d", time.Now().Unix())
108+
109+
fmt.Printf("✅ Email chat ready! Session: %s\n\n", c.sessionID)
110+
return nil
111+
}
112+
113+
// startChat runs the interactive conversation loop.
114+
func (c *emailChat) startChat(ctx context.Context) error {
115+
scanner := bufio.NewScanner(os.Stdin)
116+
117+
// Print welcome message with examples.
118+
fmt.Println("💡 Try asking questions like:")
119+
fmt.Println(" - send an email to [email protected] user:your_email password:your_password subject:subject content:content")
120+
fmt.Println()
121+
122+
for {
123+
fmt.Print("👤 You: ")
124+
if !scanner.Scan() {
125+
break
126+
}
127+
128+
userInput := strings.TrimSpace(scanner.Text())
129+
if userInput == "" {
130+
continue
131+
}
132+
133+
// Handle exit command.
134+
if strings.ToLower(userInput) == "exit" {
135+
fmt.Println("👋 Goodbye!")
136+
return nil
137+
}
138+
139+
// Process the user message.
140+
if err := c.processMessage(ctx, userInput); err != nil {
141+
fmt.Printf("❌ Error: %v\n", err)
142+
}
143+
144+
fmt.Println() // Add spacing between turns
145+
}
146+
147+
if err := scanner.Err(); err != nil {
148+
return fmt.Errorf("input scanner error: %w", err)
149+
}
150+
151+
return nil
152+
}
153+
154+
// processMessage handles a single message exchange.
155+
func (c *emailChat) processMessage(ctx context.Context, userMessage string) error {
156+
message := model.NewUserMessage(userMessage)
157+
// Run the agent through the runner.
158+
eventChan, err := c.runner.Run(ctx, c.userID, c.sessionID, message)
159+
if err != nil {
160+
return fmt.Errorf("failed to run agent: %w", err)
161+
}
162+
// Process streaming response.
163+
return c.processResponse(eventChan)
164+
}
165+
166+
// processResponse handles the response with email tool visualization.
167+
func (c *emailChat) processResponse(eventChan <-chan *event.Event) error {
168+
fmt.Print("🤖 Assistant: ")
169+
170+
var (
171+
fullContent string
172+
toolCallsDetected bool
173+
assistantStarted bool
174+
)
175+
176+
for event := range eventChan {
177+
178+
// Handle errors.
179+
if event.Error != nil {
180+
fmt.Printf("\n❌ Error: %s\n", event.Error.Message)
181+
continue
182+
}
183+
184+
// Detect and display tool calls.
185+
if len(event.Response.Choices) > 0 && len(event.Response.Choices[0].Message.ToolCalls) > 0 {
186+
toolCallsDetected = true
187+
if assistantStarted {
188+
fmt.Printf("\n")
189+
}
190+
fmt.Printf("🔍 email initiated:\n")
191+
for _, toolCall := range event.Response.Choices[0].Message.ToolCalls {
192+
fmt.Printf(" • %s (ID: %s)\n", toolCall.Function.Name, toolCall.ID)
193+
if len(toolCall.Function.Arguments) > 0 {
194+
fmt.Printf(" Query: %s\n", string(toolCall.Function.Arguments))
195+
}
196+
}
197+
fmt.Printf("\n🔄 send email...\n")
198+
}
199+
200+
// Detect tool responses.
201+
if event.Response != nil && len(event.Response.Choices) > 0 {
202+
hasToolResponse := false
203+
for _, choice := range event.Response.Choices {
204+
if choice.Message.Role == model.RoleTool && choice.Message.ToolID != "" {
205+
fmt.Printf("✅ send email results (ID: %s): %s\n",
206+
choice.Message.ToolID,
207+
strings.TrimSpace(choice.Message.Content))
208+
hasToolResponse = true
209+
}
210+
}
211+
if hasToolResponse {
212+
continue
213+
}
214+
}
215+
216+
// Process content from choices.
217+
if len(event.Response.Choices) > 0 {
218+
choice := event.Response.Choices[0]
219+
220+
if !assistantStarted {
221+
if toolCallsDetected {
222+
fmt.Printf("\n🤖 Assistant: ")
223+
}
224+
assistantStarted = true
225+
}
226+
227+
// Handle content based on streaming mode.
228+
var content string
229+
if c.streaming {
230+
// Streaming mode: use delta content.
231+
content = choice.Delta.Content
232+
} else {
233+
// Non-streaming mode: use full message content.
234+
content = choice.Message.Content
235+
}
236+
237+
if content != "" {
238+
fmt.Print(content)
239+
fullContent += content
240+
}
241+
}
242+
243+
// Check if this is the final event.
244+
if event.Done {
245+
fmt.Printf("\n")
246+
break
247+
}
248+
}
249+
250+
return nil
251+
}
252+
253+
// intPtr returns a pointer to the given int.
254+
func intPtr(i int) *int {
255+
return &i
256+
}
257+
258+
// floatPtr returns a pointer to the given float64.
259+
func floatPtr(f float64) *float64 {
260+
return &f
261+
}

examples/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,7 @@ require (
115115
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
116116
google.golang.org/grpc v1.67.0 // indirect
117117
google.golang.org/protobuf v1.34.2 // indirect
118+
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
119+
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
118120
gopkg.in/yaml.v3 v3.0.1 // indirect
119121
)

examples/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,13 @@ google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
221221
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
222222
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
223223
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
224+
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
225+
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
224226
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
225227
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
226228
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
229+
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
230+
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
227231
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
228232
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
229233
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ require (
3434
golang.org/x/text v0.21.0
3535
google.golang.org/grpc v1.65.0
3636
google.golang.org/protobuf v1.34.2
37+
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
3738
trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a
3839
trpc.group/trpc-go/trpc-mcp-go v0.0.10
3940
)
@@ -82,6 +83,7 @@ require (
8283
golang.org/x/sys v0.30.0 // indirect
8384
google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect
8485
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
86+
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
8587
gopkg.in/yaml.v3 v3.0.1 // indirect
8688
)
8789

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,13 @@ google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
180180
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
181181
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
182182
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
183+
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
184+
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
183185
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
184186
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
185187
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
188+
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
189+
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
186190
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
187191
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
188192
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

0 commit comments

Comments
 (0)