mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-06-30 06:17:12 +00:00
Compare commits
2 Commits
a2cc252e8f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 430b2cbdc0 | |||
| cab05861ba |
@@ -52,6 +52,21 @@ A scalable, multi-bot solution for Telegram using Go, GORM, and the Anthropic AP
|
||||
go build -o telegram-bot
|
||||
```
|
||||
|
||||
## Trying Out New Behavior Safely
|
||||
|
||||
Want to experiment with a different personality, tone, or set of instructions without disturbing the bot your users already talk to? Run a second, separate bot just for testing.
|
||||
|
||||
Each bot profile is its own config file with its own Telegram token, and bots are fully independent — separate identity, separate chat history, separate settings. So a "test twin" is quick to set up:
|
||||
|
||||
1. Create a new bot with [@BotFather](https://t.me/BotFather) and copy its token.
|
||||
2. Copy your existing config to a new file, e.g. `cp config/mybot.json config/mybot-test.json`.
|
||||
3. In the new file, paste the new token, give it a different `id`, and edit `system_prompts` to try your changes.
|
||||
4. Start it alongside your main bot. Chat with the test bot, tweak its prompt, and restart the test bot to try again — your real users never see the experiments.
|
||||
5. Happy with the result? Copy the same change into your main bot's config and restart it.
|
||||
|
||||
> [!NOTE]
|
||||
> A test bot always needs its **own** token. Telegram only lets one running bot listen on a given token, so you can't point a second copy at your live bot — give the twin its own @BotFather bot instead.
|
||||
|
||||
## Systemd Unit Setup
|
||||
|
||||
To enable the bot to start automatically on system boot and run in the background, set up a systemd unit.
|
||||
|
||||
@@ -27,7 +27,6 @@ type pendingAlbum struct {
|
||||
username, firstName, lastName, languageCode string
|
||||
isPremium bool
|
||||
messageTime int
|
||||
isNewChat, isOwner bool
|
||||
businessConnectionID string
|
||||
// timer flushes the album after albumFlushWindow with no further arrivals.
|
||||
// Each new arrival stops the previous timer (best-effort) and installs a
|
||||
@@ -47,7 +46,6 @@ func (b *Bot) bufferAlbumItem(
|
||||
isPremium bool,
|
||||
languageCode string,
|
||||
messageTime int,
|
||||
isNewChat, isOwner bool,
|
||||
businessConnectionID string,
|
||||
) {
|
||||
b.albumBuffersMu.Lock()
|
||||
@@ -64,8 +62,6 @@ func (b *Bot) bufferAlbumItem(
|
||||
isPremium: isPremium,
|
||||
languageCode: languageCode,
|
||||
messageTime: messageTime,
|
||||
isNewChat: isNewChat,
|
||||
isOwner: isOwner,
|
||||
businessConnectionID: businessConnectionID,
|
||||
}
|
||||
b.albumBuffers[msg.MediaGroupID] = album
|
||||
@@ -116,7 +112,6 @@ func (b *Bot) flushAlbum(ctx context.Context, mediaGroupID string) {
|
||||
captured.chatID, captured.userID,
|
||||
captured.username, captured.firstName, captured.lastName,
|
||||
captured.isPremium, captured.languageCode, captured.messageTime,
|
||||
captured.isNewChat, captured.isOwner,
|
||||
captured.businessConnectionID,
|
||||
)
|
||||
}
|
||||
|
||||
+130
-70
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
@@ -23,6 +24,37 @@ var ErrModelNotFound = errors.New("model not found or deprecated")
|
||||
// covers all realistic cascades without leaving the call hanging indefinitely.
|
||||
const maxFileNotFoundRetries = 3
|
||||
|
||||
// mcpUnsupportedSentinel is the placeholder text Anthropic's server-side MCP
|
||||
// connector substitutes when a tool result can't be serialized into a supported
|
||||
// content block. It arrives inside a normal mcp_tool_result with is_error=false,
|
||||
// so it is otherwise indistinguishable from success — every empty-result issue
|
||||
// across the ecosystem (strands #2122, openai-agents #1035, opencode #15371)
|
||||
// shows the same is_error=false on these, so we substring-match the text rather
|
||||
// than rely on the error flag. Observed trigger: an MCP server returns an empty
|
||||
// content array for a zero-result query (e.g. Outline list_documents with no
|
||||
// match), which the connector can't serialize. Substring (not the full
|
||||
// sentence) so a minor wording change upstream doesn't silently break detection.
|
||||
//
|
||||
// Scope: this catches the SOFT variant only — a streamed mcp_tool_result whose
|
||||
// content is the sentinel. The HARD variant (e.g. an unsupported image media
|
||||
// type) is a 400 that aborts the whole stream and never produces a result
|
||||
// block, so it surfaces via streamMessages' error return, not here.
|
||||
const mcpUnsupportedSentinel = "format not currently supported by the Anthropic API"
|
||||
|
||||
// mcpUnsupportedCount tallies sentinel hits across the whole process lifetime
|
||||
// (all bots, all MCP servers) so the true rate is visible — the chat masks most
|
||||
// of them because the model often degrades gracefully. The per-hit ERROR line
|
||||
// carries the bot ID and server name for attribution; this is just the running
|
||||
// total.
|
||||
var mcpUnsupportedCount atomic.Uint64
|
||||
|
||||
// mcpCall pairs a tool_use's server+name+input so a later unsupported result —
|
||||
// which arrives in a SEPARATE block linked only by tool_use_id — can name the
|
||||
// server and query that triggered it. server matters once a bot configures more
|
||||
// than one MCP server (the config supports a slice), otherwise the log can't say
|
||||
// which server choked.
|
||||
type mcpCall struct{ server, name, input string }
|
||||
|
||||
// getAnthropicResponse streams the model's response. Each completed text block
|
||||
// is delivered to onSegment as soon as the model finishes writing it — so the
|
||||
// caller can send segments to Telegram with natural rhythm around tool calls,
|
||||
@@ -34,75 +66,20 @@ const maxFileNotFoundRetries = 3
|
||||
// "File not found:" for a referenced file_id, the dead file_id is stripped
|
||||
// from this chat's in-memory ChatMemory and the corresponding DB rows are
|
||||
// stamped FilesCleanedAt so a reconciliation job can finish the cleanup.
|
||||
func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages []anthropic.BetaMessageParam, isNewChat, isOwner, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int, onSegment func(string) error) (string, error) {
|
||||
// Use prompts from config
|
||||
var systemMessage string
|
||||
if isNewChat {
|
||||
systemMessage = b.config.SystemPrompts["new_chat"]
|
||||
} else {
|
||||
systemMessage = b.config.SystemPrompts["continue_conversation"]
|
||||
}
|
||||
|
||||
// Combine default prompt with custom instructions
|
||||
systemMessage = b.config.SystemPrompts["default"] + " " + b.config.SystemPrompts["custom_instructions"] + " " + systemMessage
|
||||
|
||||
// Handle username placeholder
|
||||
usernameValue := username
|
||||
if username == "" {
|
||||
usernameValue = "unknown" // Use "unknown" when username is not available
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{username}", usernameValue)
|
||||
|
||||
// Handle firstname placeholder
|
||||
firstnameValue := firstName
|
||||
if firstName == "" {
|
||||
firstnameValue = "unknown" // Use "unknown" when first name is not available
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{firstname}", firstnameValue)
|
||||
|
||||
// Handle lastname placeholder
|
||||
lastnameValue := lastName
|
||||
if lastName == "" {
|
||||
lastnameValue = "" // Empty string when last name is not available
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{lastname}", lastnameValue)
|
||||
|
||||
// Handle language code placeholder
|
||||
langValue := languageCode
|
||||
if languageCode == "" {
|
||||
langValue = "en" // Default to English when language code is not available
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{language}", langValue)
|
||||
|
||||
// Handle premium status
|
||||
premiumStatus := "regular user"
|
||||
if isPremium {
|
||||
premiumStatus = "premium user"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
|
||||
|
||||
// Handle time awareness
|
||||
timeObj := time.Unix(int64(messageTime), 0)
|
||||
hour := timeObj.Hour()
|
||||
var timeContext string
|
||||
if hour >= 5 && hour < 12 {
|
||||
timeContext = "morning"
|
||||
} else if hour >= 12 && hour < 18 {
|
||||
timeContext = "afternoon"
|
||||
} else if hour >= 18 && hour < 22 {
|
||||
timeContext = "evening"
|
||||
} else {
|
||||
timeContext = "night"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{time_context}", timeContext)
|
||||
|
||||
if !isOwner {
|
||||
systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"]
|
||||
}
|
||||
|
||||
if isEmojiOnly {
|
||||
systemMessage += " " + b.config.SystemPrompts["respond_with_emojis"]
|
||||
}
|
||||
func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages []anthropic.BetaMessageParam, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int, onSegment func(string) error) (string, error) {
|
||||
// The system prompt is the single authored behavior driver. It is assembled
|
||||
// as a cached static block (custom_instructions) followed by a per-turn
|
||||
// dynamic tail. Prompt caching keys on a byte-identical prefix, so the static
|
||||
// block must not contain anything that changes between requests — all
|
||||
// per-turn data (who we're talking to, the time of day, the emoji-only rule)
|
||||
// lives in the trailing block, AFTER the cache breakpoint.
|
||||
//
|
||||
// An empty custom_instructions means no system prompt at all: the System
|
||||
// field is omitted entirely (not sent as a blank block), giving the model's
|
||||
// unmodified "vanilla" behavior. This matters because the Anthropic API
|
||||
// rejects a system array containing an empty/whitespace-only text block, so
|
||||
// omission is the only correct way to express "no system prompt".
|
||||
staticPrompt := strings.TrimSpace(b.config.SystemPrompts["custom_instructions"])
|
||||
|
||||
// Debug logging
|
||||
InfoLogger.Printf("Sending %d messages to Anthropic", len(messages))
|
||||
@@ -111,12 +88,32 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
|
||||
Model: b.config.Model,
|
||||
MaxTokens: 1000,
|
||||
Messages: messages,
|
||||
System: []anthropic.BetaTextBlockParam{{Text: systemMessage}},
|
||||
// Files API beta is always on: replayed conversation history may carry
|
||||
// image content blocks that reference file_ids uploaded on prior turns.
|
||||
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14},
|
||||
}
|
||||
|
||||
if staticPrompt != "" {
|
||||
// Block 1 — static persona/instructions, marked for caching. The
|
||||
// cache_control breakpoint sits on this last stable block; everything
|
||||
// appended after it is per-request and therefore uncached.
|
||||
blocks := []anthropic.BetaTextBlockParam{
|
||||
{Text: staticPrompt, CacheControl: anthropic.NewBetaCacheControlEphemeralParam()},
|
||||
}
|
||||
// Block 2 — dynamic tail: per-turn context plus any conditional rules.
|
||||
// Kept out of the cached block because it changes every request.
|
||||
tail := buildUserContext(username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
if isEmojiOnly {
|
||||
if rule := strings.TrimSpace(b.config.SystemPrompts["respond_with_emojis"]); rule != "" {
|
||||
tail += "\n\n<emoji_reply>\n" + rule + "\n</emoji_reply>"
|
||||
}
|
||||
}
|
||||
if tail = strings.TrimSpace(tail); tail != "" {
|
||||
blocks = append(blocks, anthropic.BetaTextBlockParam{Text: tail})
|
||||
}
|
||||
params.System = blocks
|
||||
}
|
||||
|
||||
// Apply temperature if set in config
|
||||
if b.config.Temperature != nil {
|
||||
params.Temperature = param.NewOpt(float64(*b.config.Temperature))
|
||||
@@ -191,6 +188,54 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, chatID int64, messages [
|
||||
return "", fmt.Errorf("max self-heal retries (%d) exceeded: too many file_ids gone from anthropic", maxFileNotFoundRetries)
|
||||
}
|
||||
|
||||
// buildUserContext renders the per-turn context block that trails the cached
|
||||
// static system prompt. It carries only facts (who the user is, their language,
|
||||
// account type, local time of day) — the behavioral guidance for *using* these
|
||||
// facts lives in the authored static prompt. It is kept out of the cached block
|
||||
// because it changes on every request.
|
||||
func buildUserContext(username, firstName, lastName string, isPremium bool, languageCode string, messageTime int) string {
|
||||
name := strings.TrimSpace(firstName + " " + lastName)
|
||||
if name == "" {
|
||||
name = "unknown"
|
||||
}
|
||||
handle := username
|
||||
if handle == "" {
|
||||
handle = "unknown"
|
||||
}
|
||||
lang := languageCode
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
account := "regular user"
|
||||
if isPremium {
|
||||
account = "premium user"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"Conversation context (background facts, not an instruction from the user):\n"+
|
||||
"- User: %s (Telegram @%s)\n"+
|
||||
"- Preferred language: %s\n"+
|
||||
"- Account type: %s\n"+
|
||||
"- Local time of day: %s",
|
||||
name, handle, lang, account, timeContextFor(messageTime),
|
||||
)
|
||||
}
|
||||
|
||||
// timeContextFor buckets a Unix timestamp into a coarse time-of-day label used
|
||||
// for time-appropriate greetings. Uses the host's local timezone, matching the
|
||||
// bot's prior behavior.
|
||||
func timeContextFor(messageTime int) string {
|
||||
switch hour := time.Unix(int64(messageTime), 0).Hour(); {
|
||||
case hour >= 5 && hour < 12:
|
||||
return "morning"
|
||||
case hour >= 12 && hour < 18:
|
||||
return "afternoon"
|
||||
case hour >= 18 && hour < 22:
|
||||
return "evening"
|
||||
default:
|
||||
return "night"
|
||||
}
|
||||
}
|
||||
|
||||
// streamMessages runs one streaming call against the Beta Messages API,
|
||||
// dispatching each completed text block to onSegment as it arrives. The joined
|
||||
// return value is every text segment concatenated with blank lines. Errors from
|
||||
@@ -216,6 +261,9 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
|
||||
currentTResultUseID, currentTResultServer string
|
||||
currentTResultIsError bool
|
||||
currentTResultContent string
|
||||
// tool_use_id -> {server, name, input}. Lives for one request; bounded
|
||||
// by the number of tool calls in the stream, so no eviction needed.
|
||||
mcpCalls = map[string]mcpCall{}
|
||||
)
|
||||
|
||||
for stream.Next() {
|
||||
@@ -269,6 +317,11 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
|
||||
}
|
||||
}
|
||||
case "mcp_tool_use":
|
||||
mcpCalls[currentTUseID] = mcpCall{
|
||||
server: currentTUseServer,
|
||||
name: currentTUseName,
|
||||
input: currentInputJSON.String(),
|
||||
}
|
||||
InfoLogger.Printf("[mcp] tool_use server=%q name=%q id=%q input=%s",
|
||||
currentTUseServer, currentTUseName, currentTUseID, currentInputJSON.String())
|
||||
case "mcp_tool_result":
|
||||
@@ -278,6 +331,13 @@ func (b *Bot) streamMessages(ctx context.Context, params anthropic.BetaMessageNe
|
||||
}
|
||||
InfoLogger.Printf("[mcp] tool_result tool_use_id=%q server=%q is_error=%v content=%s",
|
||||
currentTResultUseID, currentTResultServer, currentTResultIsError, preview)
|
||||
if strings.Contains(currentTResultContent, mcpUnsupportedSentinel) {
|
||||
total := mcpUnsupportedCount.Add(1)
|
||||
call := mcpCalls[currentTResultUseID]
|
||||
ErrorLogger.Printf("[%s][mcp][unsupported] connector could not serialize result "+
|
||||
"(total=%d): server=%q tool=%q input=%s tool_use_id=%q",
|
||||
b.config.ID, total, call.server, call.name, call.input, currentTResultUseID)
|
||||
}
|
||||
default:
|
||||
if currentKind != "" {
|
||||
InfoLogger.Printf("[mcp] block type=%q (unhandled)", currentKind)
|
||||
|
||||
+41
-176
@@ -1,197 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestLanguageCodeReplacement tests that language code is properly handled and replaced
|
||||
func TestLanguageCodeReplacement(t *testing.T) {
|
||||
// Test with provided language code
|
||||
systemMessage := "User's language preference: '{language}'"
|
||||
|
||||
// Test with a specific language code
|
||||
langValue := "fr"
|
||||
result := strings.ReplaceAll(systemMessage, "{language}", langValue)
|
||||
|
||||
if !strings.Contains(result, "User's language preference: 'fr'") {
|
||||
t.Errorf("Expected language code 'fr' to be replaced, got: %s", result)
|
||||
}
|
||||
|
||||
// Test with empty language code (should default to "en")
|
||||
langValue = ""
|
||||
if langValue == "" {
|
||||
langValue = "en" // Default to English when language code is not available
|
||||
}
|
||||
result = strings.ReplaceAll(systemMessage, "{language}", langValue)
|
||||
|
||||
if !strings.Contains(result, "User's language preference: 'en'") {
|
||||
t.Errorf("Expected default language code 'en' to be used, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPremiumStatusReplacement tests that premium status is properly handled and replaced
|
||||
func TestPremiumStatusReplacement(t *testing.T) {
|
||||
systemMessage := "User is a {premium_status}"
|
||||
|
||||
// Test with premium user
|
||||
isPremium := true
|
||||
premiumStatus := "regular user"
|
||||
if isPremium {
|
||||
premiumStatus = "premium user"
|
||||
}
|
||||
result := strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
|
||||
|
||||
if !strings.Contains(result, "User is a premium user") {
|
||||
t.Errorf("Expected premium status to be replaced with 'premium user', got: %s", result)
|
||||
}
|
||||
|
||||
// Test with regular user
|
||||
isPremium = false
|
||||
premiumStatus = "regular user"
|
||||
if isPremium {
|
||||
premiumStatus = "premium user"
|
||||
}
|
||||
result = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
|
||||
|
||||
if !strings.Contains(result, "User is a regular user") {
|
||||
t.Errorf("Expected premium status to be replaced with 'regular user', got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTimeContextCalculation tests that time context is correctly calculated for different hours
|
||||
func TestTimeContextCalculation(t *testing.T) {
|
||||
// Test cases for different hours
|
||||
testCases := []struct {
|
||||
// TestTimeContextFor verifies the time-of-day bucketing used for greetings.
|
||||
func TestTimeContextFor(t *testing.T) {
|
||||
cases := []struct {
|
||||
hour int
|
||||
expected string
|
||||
}{
|
||||
{3, "night"}, // Night: hours < 5 or hours >= 22
|
||||
{5, "morning"}, // Morning: 5 <= hours < 12
|
||||
{12, "afternoon"}, // Afternoon: 12 <= hours < 18
|
||||
{17, "afternoon"}, // Afternoon: 12 <= hours < 18
|
||||
{18, "evening"}, // Evening: 18 <= hours < 22
|
||||
{21, "evening"}, // Evening: 18 <= hours < 22
|
||||
{22, "night"}, // Night: hours < 5 or hours >= 22
|
||||
{23, "night"}, // Night: hours < 5 or hours >= 22
|
||||
{3, "night"}, // hours < 5
|
||||
{5, "morning"}, // 5 <= h < 12
|
||||
{11, "morning"}, //
|
||||
{12, "afternoon"}, // 12 <= h < 18
|
||||
{17, "afternoon"}, //
|
||||
{18, "evening"}, // 18 <= h < 22
|
||||
{21, "evening"}, //
|
||||
{22, "night"}, // h >= 22
|
||||
{23, "night"}, //
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Hour_%d", tc.hour), func(t *testing.T) {
|
||||
// Create a timestamp for the specified hour
|
||||
testTime := time.Date(2025, 5, 15, tc.hour, 0, 0, 0, time.UTC)
|
||||
|
||||
// Get the hour directly from the test time to ensure it's what we expect
|
||||
actualHour := testTime.Hour()
|
||||
if actualHour != tc.hour {
|
||||
t.Fatalf("Test setup error: expected hour %d, got %d", tc.hour, actualHour)
|
||||
for _, tc := range cases {
|
||||
// Build the timestamp in the host's local zone so timeContextFor (which
|
||||
// reads local time) observes exactly tc.hour regardless of the test
|
||||
// machine's timezone.
|
||||
ts := int(time.Date(2025, 5, 15, tc.hour, 0, 0, 0, time.Local).Unix())
|
||||
if got := timeContextFor(ts); got != tc.expected {
|
||||
t.Errorf("timeContextFor(hour=%d) = %q, want %q", tc.hour, got, tc.expected)
|
||||
}
|
||||
|
||||
// Calculate time context using the same logic as in anthropic.go
|
||||
var timeContext string
|
||||
if actualHour >= 5 && actualHour < 12 {
|
||||
timeContext = "morning"
|
||||
} else if actualHour >= 12 && actualHour < 18 {
|
||||
timeContext = "afternoon"
|
||||
} else if actualHour >= 18 && actualHour < 22 {
|
||||
timeContext = "evening"
|
||||
} else {
|
||||
timeContext = "night"
|
||||
}
|
||||
|
||||
// Check if the calculated time context matches the expected value
|
||||
if timeContext != tc.expected {
|
||||
t.Errorf("For hour %d: expected time context '%s', got '%s'",
|
||||
actualHour, tc.expected, timeContext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemMessagePlaceholderReplacement tests that all placeholders are correctly replaced
|
||||
func TestSystemMessagePlaceholderReplacement(t *testing.T) {
|
||||
systemMessage := "The user you're talking to has username '{username}' and display name '{firstname} {lastname}'.\n" +
|
||||
"User's language preference: '{language}'\n" +
|
||||
"User is a {premium_status}\n" +
|
||||
"It's currently {time_context} in your timezone"
|
||||
// TestBuildUserContext verifies the per-turn context block reflects the user's
|
||||
// details and applies the documented fallbacks.
|
||||
func TestBuildUserContext(t *testing.T) {
|
||||
noon := int(time.Date(2025, 5, 15, 12, 0, 0, 0, time.Local).Unix())
|
||||
|
||||
// Set up test data
|
||||
username := "testuser"
|
||||
firstName := "Test"
|
||||
lastName := "User"
|
||||
isPremium := true
|
||||
languageCode := "de"
|
||||
// Fully-populated premium user.
|
||||
got := buildUserContext("alice", "Alice", "Smith", true, "de", noon)
|
||||
for _, want := range []string{"Alice Smith", "@alice", "Preferred language: de", "premium user", "afternoon"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("buildUserContext premium: missing %q in:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a timestamp for a specific hour (e.g., 14:00 = afternoon)
|
||||
testTime := time.Date(2025, 5, 15, 14, 0, 0, 0, time.UTC)
|
||||
messageTime := int(testTime.Unix())
|
||||
// Missing name/username/language fall back; non-premium reads "regular user".
|
||||
got = buildUserContext("", "", "", false, "", noon)
|
||||
for _, want := range []string{"User: unknown (Telegram @unknown)", "Preferred language: en", "regular user"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("buildUserContext fallback: missing %q in:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle username placeholder
|
||||
usernameValue := username
|
||||
if username == "" {
|
||||
usernameValue = "unknown"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{username}", usernameValue)
|
||||
|
||||
// Handle firstname placeholder
|
||||
firstnameValue := firstName
|
||||
if firstName == "" {
|
||||
firstnameValue = "unknown"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{firstname}", firstnameValue)
|
||||
|
||||
// Handle lastname placeholder
|
||||
lastnameValue := lastName
|
||||
if lastName == "" {
|
||||
lastnameValue = ""
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{lastname}", lastnameValue)
|
||||
|
||||
// Handle language code placeholder
|
||||
langValue := languageCode
|
||||
if languageCode == "" {
|
||||
langValue = "en"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{language}", langValue)
|
||||
|
||||
// Handle premium status
|
||||
premiumStatus := "regular user"
|
||||
if isPremium {
|
||||
premiumStatus = "premium user"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus)
|
||||
|
||||
// Handle time awareness
|
||||
timeObj := time.Unix(int64(messageTime), 0)
|
||||
hour := timeObj.Hour()
|
||||
var timeContext string
|
||||
if hour >= 5 && hour < 12 {
|
||||
timeContext = "morning"
|
||||
} else if hour >= 12 && hour < 18 {
|
||||
timeContext = "afternoon"
|
||||
} else if hour >= 18 && hour < 22 {
|
||||
timeContext = "evening"
|
||||
} else {
|
||||
timeContext = "night"
|
||||
}
|
||||
systemMessage = strings.ReplaceAll(systemMessage, "{time_context}", timeContext)
|
||||
|
||||
// Check that all placeholders were replaced correctly
|
||||
if !strings.Contains(systemMessage, "username 'testuser'") {
|
||||
t.Errorf("Username not replaced correctly, got: %s", systemMessage)
|
||||
}
|
||||
if !strings.Contains(systemMessage, "display name 'Test User'") {
|
||||
t.Errorf("Display name not replaced correctly, got: %s", systemMessage)
|
||||
}
|
||||
if !strings.Contains(systemMessage, "language preference: 'de'") {
|
||||
t.Errorf("Language preference not replaced correctly, got: %s", systemMessage)
|
||||
}
|
||||
if !strings.Contains(systemMessage, "User is a premium user") {
|
||||
t.Errorf("Premium status not replaced correctly, got: %s", systemMessage)
|
||||
}
|
||||
if !strings.Contains(systemMessage, "It's currently afternoon in your timezone") {
|
||||
t.Errorf("Time context not replaced correctly, got: %s", systemMessage)
|
||||
// First name only (no last name) must not leave a trailing space in the name.
|
||||
got = buildUserContext("bob", "Bob", "", false, "en", noon)
|
||||
if !strings.Contains(got, "User: Bob (Telegram @bob)") {
|
||||
t.Errorf("buildUserContext firstname-only: got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,12 +355,6 @@ func contentBlocksForMessage(msg Message) []anthropic.BetaContentBlockParamUnion
|
||||
return blocks
|
||||
}
|
||||
|
||||
func (b *Bot) isNewChat(chatID int64) bool {
|
||||
var count int64
|
||||
b.db.Model(&Message{}).Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Count(&count)
|
||||
return count == 0 // Only consider a chat new if it has 0 messages
|
||||
}
|
||||
|
||||
// roleHasScope reports whether role (with pre-loaded Scopes) contains the given scope name.
|
||||
func roleHasScope(role Role, scope string) bool {
|
||||
for _, s := range role.Scopes {
|
||||
|
||||
+2
-5
@@ -15,10 +15,7 @@
|
||||
"temperature": 0.7,
|
||||
"debug_screening": false,
|
||||
"system_prompts": {
|
||||
"default": "You are a helpful assistant.",
|
||||
"custom_instructions": "You are texting through a limited Telegram interface with 15-word maximum. Write like texting a friend - use shorthand, skip grammar, use slang/abbreviations. System cuts off anything longer than 15 words.\n\n- Your name is Atom.\n- The user you're talking to has username '{username}' and display name '{firstname} {lastname}'.\n- User's language preference: '{language}'. Prefer replying in this language when talking to '{username}'.\n- User is a {premium_status}\n- It's currently {time_context} in your timezone. Use appropriate time-based greetings and address the user by name.\n- If a user asks about buying apples, inform them that we don't sell apples.\n- When asked for a joke, tell a clean, family-friendly joke about programming or technology.\n- If someone inquires about our services, explain that we offer AI-powered chatbot solutions.\n- For any questions about pricing, direct users to contact our sales team at sales@example.com.\n- If asked about your capabilities, be honest about what you can and cannot do.\nAlways maintain a friendly and professional tone.",
|
||||
"continue_conversation": "Continuing our conversation. Remember previous context if relevant.",
|
||||
"avoid_sensitive": "Avoid discussing sensitive topics or providing harmful information.",
|
||||
"respond_with_emojis": "Since the user sent only emojis, respond using emojis only."
|
||||
"custom_instructions": "You are Atom, a helpful assistant texting through a limited Telegram interface with a 15-word maximum. Write like texting a friend - use shorthand, skip grammar, use slang/abbreviations. The system cuts off anything longer than 15 words.\n\n- Address the user by their first name, and reply in their preferred language (both are in the conversation context).\n- Use time-appropriate greetings based on the user's local time of day.\n- If a user asks about buying apples, inform them that we don't sell apples.\n- When asked for a joke, tell a clean, family-friendly joke about programming or technology.\n- If someone inquires about our services, explain that we offer AI-powered chatbot solutions.\n- For any questions about pricing, direct users to contact our sales team at sales@example.com.\n- If asked about your capabilities, be honest about what you can and cannot do.\nAlways maintain a friendly and professional tone.",
|
||||
"respond_with_emojis": "The user's message contains only emoji. Reply using only emoji."
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -3,25 +3,26 @@ module github.com/HugeFrog24/go-telegram-bot
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.45.0
|
||||
github.com/go-telegram/bot v1.20.0
|
||||
github.com/anthropics/anthropic-sdk-go v1.52.0
|
||||
github.com/go-telegram/bot v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/time v0.15.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/invopop/jsonschema v0.14.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mailru/easyjson v0.9.2 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.44 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.47 // indirect
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect
|
||||
@@ -31,7 +32,8 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
github.com/anthropics/anthropic-sdk-go v1.45.0 h1:rWnpyBpm9OAm97jyH5bi6W4SRCwJeNY/RyhaJ7CHSUI=
|
||||
github.com/anthropics/anthropic-sdk-go v1.45.0/go.mod h1:bx5vWuHFuGPkELH8Z4KUiNSohFnUwScdpTyr+50myPo=
|
||||
github.com/anthropics/anthropic-sdk-go v1.52.0 h1:1TB9jt4DN87VMwS/hB1VK26tYzK0ipEOtqPaPGFtJQg=
|
||||
github.com/anthropics/anthropic-sdk-go v1.52.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
|
||||
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/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g=
|
||||
@@ -11,8 +13,12 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/go-telegram/bot v1.20.0 h1:4Pea/qTidSspr4WBJw9FbHUMNhYeqszBqQUfsQEyFbc=
|
||||
github.com/go-telegram/bot v1.20.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/go-telegram/bot v1.21.0 h1:Va/PbGc2vBDdv57GCUEEVV6ROlHWiC6SklJY9Hvhzps=
|
||||
github.com/go-telegram/bot v1.21.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
|
||||
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
@@ -28,6 +34,10 @@ github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M
|
||||
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
||||
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -53,10 +63,16 @@ 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/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=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -70,3 +86,5 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
|
||||
+8
-12
@@ -13,7 +13,7 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, userMsg Message, chatID, userID int64, username, firstName, lastName string, isPremium bool, languageCode string, messageTime int, isNewChat, isOwner bool, businessConnectionID string) {
|
||||
func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, userMsg Message, chatID, userID int64, username, firstName, lastName string, isPremium bool, languageCode string, messageTime int, businessConnectionID string) {
|
||||
// If ElevenLabs is not configured, respond with text — consistent with all other error paths.
|
||||
if b.config.ElevenLabsAPIKey == "" {
|
||||
if err := b.sendResponse(ctx, chatID, "I don't understand voice messages.", businessConnectionID); err != nil {
|
||||
@@ -59,7 +59,7 @@ func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, u
|
||||
// Voice path passes nil for onSegment: tool-call narration across multiple
|
||||
// TTS clips would be jarring, so we accumulate everything and synthesize one
|
||||
// audio clip from the joined text.
|
||||
response, err := b.getAnthropicResponse(ctx, chatID, contextMessages, isNewChat, isOwner, false, username, firstName, lastName, isPremium, languageCode, messageTime, nil)
|
||||
response, err := b.getAnthropicResponse(ctx, chatID, contextMessages, false, username, firstName, lastName, isPremium, languageCode, messageTime, nil)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting Anthropic response for voice: %v", err)
|
||||
if err := b.sendResponse(ctx, chatID, b.anthropicErrorResponse(err, userID), businessConnectionID); err != nil {
|
||||
@@ -125,7 +125,6 @@ func (b *Bot) handlePhotoMessage(
|
||||
isPremium bool,
|
||||
languageCode string,
|
||||
messageTime int,
|
||||
isNewChat, isOwner bool,
|
||||
businessConnectionID string,
|
||||
) {
|
||||
if len(items) == 0 {
|
||||
@@ -203,7 +202,7 @@ func (b *Bot) handlePhotoMessage(
|
||||
// Phase 3: stream Anthropic's reply, same shape as the text path.
|
||||
contextMessages := b.prepareContextMessages(chatMemory)
|
||||
joined, err := b.getAnthropicResponse(
|
||||
ctx, chatID, contextMessages, isNewChat, isOwner, false,
|
||||
ctx, chatID, contextMessages, false,
|
||||
username, firstName, lastName, isPremium, languageCode, messageTime,
|
||||
func(seg string) error {
|
||||
return b.sendOneSegment(ctx, chatID, seg, businessConnectionID)
|
||||
@@ -292,9 +291,6 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
messageTime := message.Date
|
||||
text := message.Text
|
||||
|
||||
// Check if it's a new chat (before storing the message so the flag is accurate).
|
||||
isNewChatFlag := b.isNewChat(chatID)
|
||||
|
||||
// Determine if the user is the owner — needed up-front so the album buffer
|
||||
// can capture it alongside other per-turn metadata.
|
||||
var isOwner bool
|
||||
@@ -323,7 +319,7 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
// the flush dispatches to handlePhotoMessage.
|
||||
if message.MediaGroupID != "" && len(message.Photo) > 0 {
|
||||
b.bufferAlbumItem(ctx, message, chatID, userID, username, firstName, lastName,
|
||||
isPremium, languageCode, messageTime, isNewChatFlag, isOwner, businessConnectionID)
|
||||
isPremium, languageCode, messageTime, businessConnectionID)
|
||||
return
|
||||
}
|
||||
if len(message.Photo) > 0 {
|
||||
@@ -334,7 +330,7 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
b.handlePhotoMessage(ctx, []*models.Message{message},
|
||||
chatID, userID, username, firstName, lastName,
|
||||
isPremium, languageCode, messageTime,
|
||||
isNewChatFlag, isOwner, businessConnectionID)
|
||||
businessConnectionID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -490,7 +486,7 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
// Check if the message contains a voice note (context is built inside the handler
|
||||
// after the transcript replaces the placeholder, so it must not be built here).
|
||||
if message.Voice != nil {
|
||||
b.handleVoiceMessage(ctx, message, userMsg, chatID, userID, username, firstName, lastName, isPremium, languageCode, messageTime, isNewChatFlag, isOwner, businessConnectionID)
|
||||
b.handleVoiceMessage(ctx, message, userMsg, chatID, userID, username, firstName, lastName, isPremium, languageCode, messageTime, businessConnectionID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -518,7 +514,7 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
// calls (text → pause for tool → text → pause → text), rather than a long
|
||||
// upfront wait followed by all bubbles at once.
|
||||
joined, err := b.getAnthropicResponse(
|
||||
ctx, chatID, contextMessages, isNewChatFlag, isOwner, isEmojiOnly,
|
||||
ctx, chatID, contextMessages, isEmojiOnly,
|
||||
username, firstName, lastName, isPremium, languageCode, messageTime,
|
||||
func(seg string) error {
|
||||
return b.sendOneSegment(ctx, chatID, seg, businessConnectionID)
|
||||
@@ -578,7 +574,7 @@ func (b *Bot) generateStickerResponse(ctx context.Context, message Message, cont
|
||||
messageTime := int(message.Timestamp.Unix())
|
||||
// Sticker reactions are casual chit-chat; tool use is unusual here, so
|
||||
// pass nil for onSegment and return the joined text for a single bubble.
|
||||
response, err := b.getAnthropicResponse(ctx, message.ChatID, contextMessages, false, false, true, message.Username, "", "", false, "", messageTime, nil)
|
||||
response, err := b.getAnthropicResponse(ctx, message.ChatID, contextMessages, true, message.Username, "", "", false, "", messageTime, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user