feat: integrate Google services and WhatsApp QR command
This commit is contained in:
parent
d2366b4785
commit
5edf1cb27e
13 changed files with 338 additions and 1 deletions
10
debug_home.go
Normal file
10
debug_home.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package main
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
func main() {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
fmt.Printf("Home: %s\n", home)
|
||||||
|
fmt.Printf("PICOCLAW_HOME: %s\n", os.Getenv("PICOCLAW_HOME"))
|
||||||
|
}
|
||||||
|
|
@ -79,6 +79,9 @@ func NewAgentInstance(
|
||||||
if cfg.Tools.IsToolEnabled("list_dir") {
|
if cfg.Tools.IsToolEnabled("list_dir") {
|
||||||
toolsRegistry.Register(tools.NewListDirTool(workspace, readRestrict, allowReadPaths))
|
toolsRegistry.Register(tools.NewListDirTool(workspace, readRestrict, allowReadPaths))
|
||||||
}
|
}
|
||||||
|
if cfg.Tools.IsToolEnabled("google") {
|
||||||
|
toolsRegistry.Register(&tools.GoogleTool{})
|
||||||
|
}
|
||||||
if cfg.Tools.IsToolEnabled("exec") {
|
if cfg.Tools.IsToolEnabled("exec") {
|
||||||
execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg)
|
execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1833,6 +1833,12 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
GetChannel: func(name string) (any, bool) {
|
||||||
|
if al.channelManager == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return al.channelManager.GetChannel(name)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if agent != nil {
|
if agent != nil {
|
||||||
rt.GetModelInfo = func() (string, string) {
|
rt.GetModelInfo = func() (string, string) {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func GoogleAntigravityOAuthConfig() OAuthProviderConfig {
|
||||||
TokenURL: "https://oauth2.googleapis.com/token",
|
TokenURL: "https://oauth2.googleapis.com/token",
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
Scopes: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/cclog https://www.googleapis.com/auth/experimentsandconfigs",
|
Scopes: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/cclog https://www.googleapis.com/auth/experimentsandconfigs https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/calendar.readonly",
|
||||||
Port: 51121,
|
Port: 51121,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,9 @@ type PlaceholderRecorder interface {
|
||||||
type CommandRegistrarCapable interface {
|
type CommandRegistrarCapable interface {
|
||||||
RegisterCommands(ctx context.Context, defs []commands.Definition) error
|
RegisterCommands(ctx context.Context, defs []commands.Definition) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QRProvider is implemented by channels that can provide a QR code string
|
||||||
|
// (e.g. for WhatsApp pairing).
|
||||||
|
type QRProvider interface {
|
||||||
|
GetLastQR() string
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ type WhatsAppNativeChannel struct {
|
||||||
reconnecting bool
|
reconnecting bool
|
||||||
stopping atomic.Bool // set once Stop begins; prevents new wg.Add calls
|
stopping atomic.Bool // set once Stop begins; prevents new wg.Add calls
|
||||||
wg sync.WaitGroup // tracks background goroutines (QR handler, reconnect)
|
wg sync.WaitGroup // tracks background goroutines (QR handler, reconnect)
|
||||||
|
lastQR string // stores the last QR code string for retrieval
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWhatsAppNativeChannel creates a WhatsApp channel that uses whatsmeow for connection.
|
// NewWhatsAppNativeChannel creates a WhatsApp channel that uses whatsmeow for connection.
|
||||||
|
|
@ -187,6 +188,9 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
if evt.Event == "code" {
|
if evt.Event == "code" {
|
||||||
logger.InfoCF("whatsapp", "Scan this QR code with WhatsApp (Linked Devices):", nil)
|
logger.InfoCF("whatsapp", "Scan this QR code with WhatsApp (Linked Devices):", nil)
|
||||||
|
c.mu.Lock()
|
||||||
|
c.lastQR = evt.Code
|
||||||
|
c.mu.Unlock()
|
||||||
qrterminal.GenerateWithConfig(evt.Code, qrterminal.Config{
|
qrterminal.GenerateWithConfig(evt.Code, qrterminal.Config{
|
||||||
Level: qrterminal.L,
|
Level: qrterminal.L,
|
||||||
Writer: os.Stdout,
|
Writer: os.Stdout,
|
||||||
|
|
@ -446,3 +450,9 @@ func parseJID(s string) (types.JID, error) {
|
||||||
}
|
}
|
||||||
return types.NewJID(s, types.DefaultUserServer), nil
|
return types.NewJID(s, types.DefaultUserServer), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *WhatsAppNativeChannel) GetLastQR() string {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.lastQR
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,6 @@ func BuiltinDefinitions() []Definition {
|
||||||
switchCommand(),
|
switchCommand(),
|
||||||
checkCommand(),
|
checkCommand(),
|
||||||
clearCommand(),
|
clearCommand(),
|
||||||
|
whatsappCommand(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
pkg/commands/cmd_whatsapp.go
Normal file
51
pkg/commands/cmd_whatsapp.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
|
)
|
||||||
|
|
||||||
|
func whatsappCommand() Definition {
|
||||||
|
return Definition{
|
||||||
|
Name: "whatsapp",
|
||||||
|
Description: "WhatsApp management commands",
|
||||||
|
Subcommands: []Definition{
|
||||||
|
{
|
||||||
|
Name: "qr",
|
||||||
|
Description: "Get the latest WhatsApp pairing QR code",
|
||||||
|
Handler: func(ctx context.Context, req Request, rt Runtime) ExecuteResult {
|
||||||
|
ch, ok := rt.GetChannel("whatsapp_native")
|
||||||
|
if !ok {
|
||||||
|
return ExecuteResult{
|
||||||
|
Outcome: OutcomeHandled,
|
||||||
|
Err: fmt.Errorf("whatsapp_native channel is not enabled"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qrProvider, ok := ch.(channels.QRProvider)
|
||||||
|
if !ok {
|
||||||
|
return ExecuteResult{
|
||||||
|
Outcome: OutcomeHandled,
|
||||||
|
Err: fmt.Errorf("whatsapp_native channel does not support QR retrieval"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qr := qrProvider.GetLastQR()
|
||||||
|
if qr == "" {
|
||||||
|
return ExecuteResult{
|
||||||
|
Outcome: OutcomeHandled,
|
||||||
|
Err: fmt.Errorf("no QR code available yet. please wait for the channel to initialize"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return the QR code string.
|
||||||
|
// Optimization: In the future, we can return an image reference.
|
||||||
|
_ = req.Reply(fmt.Sprintf("Scan this QR code string (or wait for image support): %s", qr))
|
||||||
|
|
||||||
|
return ExecuteResult{Outcome: OutcomeHandled}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,4 +14,5 @@ type Runtime struct {
|
||||||
SwitchModel func(value string) (oldModel string, err error)
|
SwitchModel func(value string) (oldModel string, err error)
|
||||||
SwitchChannel func(value string) error
|
SwitchChannel func(value string) error
|
||||||
ClearHistory func() error
|
ClearHistory func() error
|
||||||
|
GetChannel func(name string) (any, bool)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -689,6 +689,7 @@ type ToolsConfig struct {
|
||||||
FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
|
FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"`
|
||||||
I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"`
|
I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"`
|
||||||
InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
|
InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"`
|
||||||
|
Google ToolConfig `json:"google" envPrefix:"PICOCLAW_TOOLS_GOOGLE_"`
|
||||||
ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
|
ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"`
|
||||||
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
|
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
|
||||||
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
|
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
|
||||||
|
|
@ -953,6 +954,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
|
||||||
return t.I2C.Enabled
|
return t.I2C.Enabled
|
||||||
case "install_skill":
|
case "install_skill":
|
||||||
return t.InstallSkill.Enabled
|
return t.InstallSkill.Enabled
|
||||||
|
case "google":
|
||||||
|
return t.Google.Enabled
|
||||||
case "list_dir":
|
case "list_dir":
|
||||||
return t.ListDir.Enabled
|
return t.ListDir.Enabled
|
||||||
case "message":
|
case "message":
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,9 @@ func DefaultConfig() *Config {
|
||||||
InstallSkill: ToolConfig{
|
InstallSkill: ToolConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
|
Google: ToolConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
ListDir: ToolConfig{
|
ListDir: ToolConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
205
pkg/tools/google.go
Normal file
205
pkg/tools/google.go
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoogleTool struct{}
|
||||||
|
|
||||||
|
func (t *GoogleTool) Name() string {
|
||||||
|
return "google"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GoogleTool) Description() string {
|
||||||
|
return "Access Google services like Gmail and Calendar. Actions: 'list_emails', 'list_events'. Use 'list_emails' to get recent messages (subject, snippet). Use 'list_events' to get upcoming calendar events (summary, start/end time)."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GoogleTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"action": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"enum": []string{"list_emails", "list_events"},
|
||||||
|
"description": "The service action to perform.",
|
||||||
|
},
|
||||||
|
"count": map[string]any{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 10,
|
||||||
|
"description": "Number of items to retrieve.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"action"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GoogleTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
action, _ := args["action"].(string)
|
||||||
|
count := 10
|
||||||
|
if c, ok := args["count"].(float64); ok {
|
||||||
|
count = int(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, err := auth.GetCredential("google-antigravity")
|
||||||
|
if err != nil || cred == nil {
|
||||||
|
return ErrorResult("Google account not linked. User must authenticate via 'google' provider first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatic refresh if needed
|
||||||
|
if cred.NeedsRefresh() {
|
||||||
|
logger.InfoC("tools", "Refreshing Google access token")
|
||||||
|
newCred, err := auth.RefreshAccessToken(cred, auth.GoogleAntigravityOAuthConfig())
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("Failed to refresh Google token: %v", err))
|
||||||
|
}
|
||||||
|
cred = newCred
|
||||||
|
_ = auth.SetCredential("google-antigravity", cred)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "list_emails":
|
||||||
|
return t.listEmails(ctx, cred, count)
|
||||||
|
case "list_events":
|
||||||
|
return t.listEvents(ctx, cred, count)
|
||||||
|
default:
|
||||||
|
return ErrorResult("Unknown action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GoogleTool) listEmails(ctx context.Context, cred *auth.AuthCredential, maxResults int) *ToolResult {
|
||||||
|
url := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=%d", maxResults)
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+cred.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("API request failed: %v", err))
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return ErrorResult(fmt.Sprintf("Gmail API error (%d): %s", resp.StatusCode, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp struct {
|
||||||
|
Messages []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"messages"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("Failed to decode Gmail list: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var emails []string
|
||||||
|
for _, m := range listResp.Messages {
|
||||||
|
msgURL := "https://gmail.googleapis.com/gmail/v1/users/me/messages/" + m.ID
|
||||||
|
mReq, _ := http.NewRequestWithContext(ctx, "GET", msgURL, nil)
|
||||||
|
mReq.Header.Set("Authorization", "Bearer "+cred.AccessToken)
|
||||||
|
mResp, err := http.DefaultClient.Do(mReq)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var msg struct {
|
||||||
|
Snippet string `json:"snippet"`
|
||||||
|
Payload struct {
|
||||||
|
Headers []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"headers"`
|
||||||
|
} `json:"payload"`
|
||||||
|
}
|
||||||
|
_ = json.NewDecoder(mResp.Body).Decode(&msg)
|
||||||
|
mResp.Body.Close()
|
||||||
|
|
||||||
|
subject := "No Subject"
|
||||||
|
from := "Unknown"
|
||||||
|
for _, h := range msg.Payload.Headers {
|
||||||
|
if h.Name == "Subject" {
|
||||||
|
subject = h.Value
|
||||||
|
} else if h.Name == "From" {
|
||||||
|
from = h.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emails = append(emails, fmt.Sprintf("- From: %s\n Subject: %s\n Snippet: %s", from, subject, msg.Snippet))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(emails) == 0 {
|
||||||
|
return SilentResult("No messages found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return SilentResult(fmt.Sprintf("Recent Emails:\n%s", join(emails, "\n\n")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *GoogleTool) listEvents(ctx context.Context, cred *auth.AuthCredential, maxResults int) *ToolResult {
|
||||||
|
now := time.Now().Format(time.RFC3339)
|
||||||
|
url := fmt.Sprintf("https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=%s&maxResults=%d&singleEvents=true&orderBy=startTime", now, maxResults)
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+cred.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("API request failed: %v", err))
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return ErrorResult(fmt.Sprintf("Calendar API error (%d): %s", resp.StatusCode, string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventList struct {
|
||||||
|
Items []struct {
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Start struct {
|
||||||
|
DateTime string `json:"dateTime"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
} `json:"start"`
|
||||||
|
End struct {
|
||||||
|
DateTime string `json:"dateTime"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
} `json:"end"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&eventList); err != nil {
|
||||||
|
return ErrorResult(fmt.Sprintf("Failed to decode Calendar events: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []string
|
||||||
|
for _, it := range eventList.Items {
|
||||||
|
start := it.Start.DateTime
|
||||||
|
if start == "" {
|
||||||
|
start = it.Start.Date
|
||||||
|
}
|
||||||
|
end := it.End.DateTime
|
||||||
|
if end == "" {
|
||||||
|
end = it.End.Date
|
||||||
|
}
|
||||||
|
events = append(events, fmt.Sprintf("- Event: %s\n Start: %s\n End: %s", it.Summary, start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(events) == 0 {
|
||||||
|
return SilentResult("No upcoming events found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return SilentResult(fmt.Sprintf("Upcoming Calendar Events:\n%s", join(events, "\n")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func join(s []string, sep string) string {
|
||||||
|
res := ""
|
||||||
|
for i, v := range s {
|
||||||
|
if i > 0 {
|
||||||
|
res += sep
|
||||||
|
}
|
||||||
|
res += v
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
38
workspace/skills/google_sync/SKILL.md
Normal file
38
workspace/skills/google_sync/SKILL.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
name: google_sync
|
||||||
|
description: Synchronize Google services (Email, Calendar) with PicoClaw
|
||||||
|
---
|
||||||
|
|
||||||
|
# Google Sync Skill
|
||||||
|
|
||||||
|
This skill allows the agent to synchronize and manage user's Google services.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### `google`
|
||||||
|
Provides access to Gmail and Google Calendar.
|
||||||
|
- `action="list_emails"`: Fetches recent emails.
|
||||||
|
- `action="list_events"`: Fetches upcoming calendar events.
|
||||||
|
|
||||||
|
## Periodic Synchronization
|
||||||
|
|
||||||
|
To keep the agent's knowledge up-to-date, use the `cron` tool to schedule periodic sync tasks.
|
||||||
|
|
||||||
|
### Example: Sync Every 4 Hours
|
||||||
|
Call `cron` tool:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"message": "Update my knowledge of recent emails and calendar events using the google tool.",
|
||||||
|
"every_seconds": 14400,
|
||||||
|
"deliver": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-Syncing Implementation
|
||||||
|
|
||||||
|
When triggered by cron, the agent should:
|
||||||
|
1. Call `google(action="list_emails")`
|
||||||
|
2. Call `google(action="list_events")`
|
||||||
|
3. Summarize the findings.
|
||||||
|
4. Update its long-term knowledge or session summary.
|
||||||
Loading…
Add table
Reference in a new issue