Files
Memoh/internal/agent/tools/contacts.go
T
Acbox da2e999ce3 feat: searchable timezone select & bot timezone priority
- Add reusable TimezoneSelect component with search and UTC offset labels
- Replace plain Select with searchable TimezoneSelect in profile settings,
  bot settings, and browser context settings
- Move bot timezone setting from header dialog into bot settings tab
- Resolve timezone with bot > user > system priority for all LLM-facing
  time formatting (user message header, system prompt, heartbeat, tools,
  memory extraction)
- Format tool output timestamps (history, contacts) in resolved timezone
2026-03-26 21:00:21 +08:00

95 lines
2.7 KiB
Go

package tools
import (
"context"
"errors"
"log/slog"
"strings"
sdk "github.com/memohai/twilight-ai/sdk"
"github.com/memohai/memoh/internal/channel/route"
)
type ContactsProvider struct {
routeService route.Service
logger *slog.Logger
}
func NewContactsProvider(log *slog.Logger, routeService route.Service) *ContactsProvider {
if log == nil {
log = slog.Default()
}
return &ContactsProvider{
routeService: routeService,
logger: log.With(slog.String("tool", "contacts")),
}
}
func (p *ContactsProvider) Tools(_ context.Context, session SessionContext) ([]sdk.Tool, error) {
if session.IsSubagent || p.routeService == nil {
return nil, nil
}
sess := session
return []sdk.Tool{
{
Name: "get_contacts",
Description: "List all known contacts and conversations for the current bot. Returns platform, conversation type, reply target, and metadata for each route.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"platform": map[string]any{
"type": "string",
"description": "Filter by channel platform (e.g. telegram, feishu). Returns all platforms when omitted.",
},
},
"required": []string{},
},
Execute: func(ctx *sdk.ToolExecContext, input any) (any, error) {
args := inputAsMap(input)
botID := strings.TrimSpace(sess.BotID)
if botID == "" {
return nil, errors.New("bot_id is required")
}
routes, err := p.routeService.List(ctx.Context, botID)
if err != nil {
return nil, err
}
platformFilter := strings.ToLower(strings.TrimSpace(FirstStringArg(args, "platform")))
contacts := make([]map[string]any, 0, len(routes))
for _, r := range routes {
if platformFilter != "" && !strings.EqualFold(r.Platform, platformFilter) {
continue
}
entry := map[string]any{
"route_id": r.ID,
"platform": r.Platform,
"conversation_type": r.ConversationType,
"target": r.ReplyTarget,
"conversation_id": r.ConversationID,
"last_active": sess.FormatTime(r.UpdatedAt),
}
if len(r.Metadata) > 0 {
if v, ok := r.Metadata["conversation_name"].(string); ok && v != "" {
entry["display_name"] = v
} else if v, ok := r.Metadata["sender_display_name"].(string); ok && v != "" {
entry["display_name"] = v
}
if v, ok := r.Metadata["sender_username"].(string); ok && v != "" {
entry["username"] = v
}
entry["metadata"] = r.Metadata
}
contacts = append(contacts, entry)
}
return map[string]any{
"ok": true,
"bot_id": botID,
"count": len(contacts),
"contacts": contacts,
}, nil
},
},
}, nil
}