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
This commit is contained in:
Acbox
2026-03-26 21:00:21 +08:00
parent 65b2797626
commit da2e999ce3
18 changed files with 259 additions and 206 deletions
@@ -0,0 +1,63 @@
<template>
<SearchableSelectPopover
v-model="selected"
:options="options"
:placeholder="placeholder || ''"
:aria-label="placeholder || 'Select timezone'"
:search-placeholder="$t('common.searchTimezone')"
search-aria-label="Search timezones"
:empty-text="$t('common.noTimezoneFound')"
:show-group-headers="false"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchableSelectPopover from '@/components/searchable-select-popover/index.vue'
import type { SearchableSelectOption } from '@/components/searchable-select-popover/index.vue'
import { timezones, emptyTimezoneValue, getUtcOffsetLabel } from '@/utils/timezones'
const { t } = useI18n()
const props = withDefaults(defineProps<{
placeholder?: string
allowEmpty?: boolean
emptyLabel?: string
}>(), {
placeholder: '',
allowEmpty: false,
emptyLabel: '',
})
const selected = defineModel<string>({ default: '' })
const offsetMap = computed(() => {
const map = new Map<string, string>()
for (const tz of timezones) {
map.set(tz, getUtcOffsetLabel(tz))
}
return map
})
const options = computed<SearchableSelectOption[]>(() => {
const items: SearchableSelectOption[] = []
if (props.allowEmpty) {
items.push({
value: emptyTimezoneValue,
label: props.emptyLabel || t('bots.timezoneInherited'),
})
}
for (const tz of timezones) {
const parts = tz.split('/')
const offset = offsetMap.value.get(tz) ?? ''
items.push({
value: tz,
label: tz,
description: offset,
keywords: [...parts, offset],
})
}
return items
})
</script>
+3 -1
View File
@@ -34,7 +34,9 @@
"loadFailed": "Failed to load",
"saveFailed": "Failed to save",
"createdAt": "Created at",
"none": "None"
"none": "None",
"searchTimezone": "Search timezones…",
"noTimezoneFound": "No timezone found."
},
"auth": {
"welcome": "Welcome Back",
+3 -1
View File
@@ -34,7 +34,9 @@
"loadFailed": "加载失败",
"saveFailed": "保存失败",
"createdAt": "创建时间",
"none": "无"
"none": "无",
"searchTimezone": "搜索时区…",
"noTimezoneFound": "未找到时区"
},
"auth": {
"welcome": "欢迎回来",
@@ -197,6 +197,21 @@
/>
</div>
<!-- Timezone -->
<div class="space-y-2">
<Label>{{ $t('bots.timezone') }}</Label>
<TimezoneSelect
:model-value="timezoneModel"
:placeholder="$t('bots.timezonePlaceholder')"
allow-empty
:empty-label="$t('bots.timezoneInherited')"
@update:model-value="onTimezoneChange"
/>
<p class="text-xs text-muted-foreground">
{{ $t('bots.timezoneInheritedHint') }}
</p>
</div>
<Separator />
<!-- Max Context Load Time -->
@@ -334,18 +349,20 @@ import {
SelectTrigger,
SelectValue,
} from '@memohai/ui'
import { reactive, computed, watch } from 'vue'
import { reactive, computed, watch, ref } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { useI18n } from 'vue-i18n'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import TimezoneSelect from '@/components/timezone-select/index.vue'
import { emptyTimezoneValue } from '@/utils/timezones'
import ModelSelect from './model-select.vue'
import SearchProviderSelect from './search-provider-select.vue'
import MemoryProviderSelect from './memory-provider-select.vue'
import TtsModelSelect from './tts-model-select.vue'
import BrowserContextSelect from './browser-context-select.vue'
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
import { getBotsByBotIdSettings, putBotsByBotIdSettings, deleteBotsById, getModels, getProviders, getSearchProviders, getMemoryProviders, getTtsProviders, getBrowserContexts, getBotsByBotIdMemoryStatus, postBotsByBotIdMemoryRebuild } from '@memohai/sdk'
import { getBotsById, putBotsById, getBotsByBotIdSettings, putBotsByBotIdSettings, deleteBotsById, getModels, getProviders, getSearchProviders, getMemoryProviders, getTtsProviders, getBrowserContexts, getBotsByBotIdMemoryStatus, postBotsByBotIdMemoryRebuild } from '@memohai/sdk'
import type { SettingsSettings } from '@memohai/sdk'
import type { Ref } from 'vue'
import { resolveApiErrorMessage } from '@/utils/api-error'
@@ -362,6 +379,15 @@ const botIdRef = computed(() => props.botId) as Ref<string>
// ---- Data ----
const queryCache = useQueryCache()
const { data: bot } = useQuery({
key: () => ['bot', botIdRef.value],
query: async () => {
const { data } = await getBotsById({ path: { id: botIdRef.value }, throwOnError: true })
return data
},
enabled: () => !!botIdRef.value,
})
const { data: settings } = useQuery({
key: () => ['bot-settings', botIdRef.value],
query: async () => {
@@ -477,6 +503,31 @@ const form = reactive({
reasoning_effort: 'medium',
})
const timezone = ref('')
const timezoneModel = computed(() => timezone.value || emptyTimezoneValue)
function onTimezoneChange(value: string) {
timezone.value = value === emptyTimezoneValue ? '' : value
}
watch(bot, (val) => {
if (val) {
timezone.value = val.timezone || ''
}
}, { immediate: true })
const { mutateAsync: updateBot } = useMutation({
mutation: async ({ id, ...body }: Record<string, unknown> & { id: string }) => {
const { data } = await putBotsById({ path: { id }, body, throwOnError: true })
return data
},
onSettled: () => {
queryCache.invalidateQueries({ key: ['bots'] })
queryCache.invalidateQueries({ key: ['bot'] })
},
})
const selectedMemoryProvider = computed(() =>
memoryProviders.value.find((provider) => provider.id === form.memory_provider_id),
)
@@ -590,10 +641,12 @@ watch(settings, (val) => {
}, { immediate: true })
const hasChanges = computed(() => {
const timezoneChanged = timezone.value !== (bot.value?.timezone || '')
if (!settings.value) return true
const s = settings.value
let changed =
form.chat_model_id !== (s.chat_model_id ?? '')
timezoneChanged
|| form.chat_model_id !== (s.chat_model_id ?? '')
|| form.title_model_id !== (s.title_model_id ?? '')
|| form.search_provider_id !== (s.search_provider_id ?? '')
|| form.memory_provider_id !== (s.memory_provider_id ?? '')
@@ -609,7 +662,11 @@ const hasChanges = computed(() => {
async function handleSave() {
try {
await updateSettings({ ...form })
const promises: Promise<unknown>[] = [updateSettings({ ...form })]
if (timezone.value !== (bot.value?.timezone || '')) {
promises.push(updateBot({ id: botIdRef.value, timezone: timezone.value }))
}
await Promise.all(promises)
toast.success(t('bots.settings.saveSuccess'))
} catch {
return
-112
View File
@@ -93,22 +93,6 @@
</Badge>
<span v-if="bot?.type">{{ botTypeLabel }}</span>
</div>
<div
v-if="bot"
class="mt-1 flex items-center gap-2 text-sm text-muted-foreground"
>
<span>{{ $t('bots.timezone') }}: {{ bot.timezone || $t('bots.timezoneInherited') }}</span>
<Button
type="button"
variant="ghost"
size="sm"
class="h-7 px-2"
:disabled="botLifecyclePending"
@click="handleEditTimezone"
>
{{ $t('common.edit') }}
</Button>
</div>
</div>
</div>
<Separator />
@@ -209,67 +193,6 @@
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="timezoneDialogOpen">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ $t('bots.editTimezone') }}</DialogTitle>
<DialogDescription>
{{ $t('bots.editTimezoneDescription') }}
</DialogDescription>
</DialogHeader>
<div class="mt-4 flex flex-col gap-2">
<Select
:model-value="timezoneDraft || emptyTimezoneValue"
@update:model-value="(value) => timezoneDraft = value === emptyTimezoneValue ? '' : String(value)"
>
<SelectTrigger
class="w-full"
:disabled="timezoneSaving"
>
<SelectValue :placeholder="$t('bots.timezonePlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="emptyTimezoneValue">
{{ $t('bots.timezoneInherited') }}
</SelectItem>
<SelectItem
v-for="timezoneOption in timezones"
:key="timezoneOption"
:value="timezoneOption"
>
{{ timezoneOption }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<p class="text-sm text-muted-foreground">
{{ $t('bots.timezoneInheritedHint') }}
</p>
</div>
<DialogFooter class="mt-6">
<DialogClose as-child>
<Button
variant="outline"
:disabled="timezoneSaving"
>
{{ $t('common.cancel') }}
</Button>
</DialogClose>
<Button
:disabled="timezoneSaving || !canConfirmTimezone"
@click="handleConfirmTimezone"
>
<Spinner
v-if="timezoneSaving"
class="mr-1.5"
/>
{{ $t('common.confirm') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
</template>
@@ -288,12 +211,6 @@ import {
DialogHeader,
DialogTitle,
Input,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Spinner,
ScrollArea,
@@ -338,7 +255,6 @@ import { useAvatarInitials } from '@/composables/useAvatarInitials'
import { useSyncedQueryParam } from '@/composables/useSyncedQueryParam'
import { useBotStatusMeta } from '@/composables/useBotStatusMeta'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
import { emptyTimezoneValue, timezones } from '@/utils/timezones'
type BotCheck = BotsBotCheck
type BotContainerInfo = HandlersGetContainerResponse
type BotContainerSnapshot = HandlersListSnapshotsResponse extends { snapshots?: (infer T)[] } ? T : never
@@ -428,12 +344,9 @@ watch(bot, (val) => {
const activeTab = useSyncedQueryParam('tab', 'overview')
const avatarDialogOpen = ref(false)
const avatarUrlDraft = ref('')
const timezoneDialogOpen = ref(false)
const timezoneDraft = ref('')
const avatarFallback = useAvatarInitials(() => bot.value?.display_name || botId.value || '')
const isSavingBotName = computed(() => updateBotLoading.value)
const avatarSaving = computed(() => updateBotLoading.value)
const timezoneSaving = computed(() => updateBotLoading.value)
const canConfirmAvatar = computed(() => {
if (!bot.value) return false
const next = avatarUrlDraft.value.trim()
@@ -446,10 +359,6 @@ const canConfirmBotName = computed(() => {
if (!nextName) return false
return nextName !== (bot.value.display_name || '').trim()
})
const canConfirmTimezone = computed(() => {
if (!bot.value) return false
return timezoneDraft.value.trim() !== (bot.value.timezone || '').trim()
})
const {
hasIssue,
isPending: botLifecyclePending,
@@ -503,12 +412,6 @@ function handleEditAvatar() {
avatarDialogOpen.value = true
}
function handleEditTimezone() {
if (!bot.value || botLifecyclePending.value) return
timezoneDraft.value = bot.value.timezone || ''
timezoneDialogOpen.value = true
}
async function handleConfirmAvatar() {
if (!bot.value || !canConfirmAvatar.value || avatarSaving.value) return
const nextUrl = avatarUrlDraft.value.trim()
@@ -524,21 +427,6 @@ async function handleConfirmAvatar() {
}
}
async function handleConfirmTimezone() {
if (!bot.value || !canConfirmTimezone.value || timezoneSaving.value) return
const nextTimezone = timezoneDraft.value.trim()
try {
await updateBot({
id: bot.value.id as string,
timezone: nextTimezone,
})
timezoneDialogOpen.value = false
toast.success(t('bots.timezoneUpdateSuccess'))
} catch (error) {
toast.error(resolveErrorMessage(error, t('bots.timezoneUpdateFailed')))
}
}
function handleStartEditBotName() {
if (!bot.value) return
isEditingBotName.value = true
@@ -149,34 +149,19 @@
</div>
<FormField
v-slot="{ componentField }"
v-slot="{ value, handleChange }"
name="timezoneId"
>
<FormItem>
<Label>{{ $t('browserContext.timezoneId') }}</Label>
<FormControl>
<Select
:model-value="componentField.modelValue || emptyTimezoneValue"
@update:model-value="(value) => componentField['onUpdate:modelValue'](value === emptyTimezoneValue ? '' : value)"
>
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('browserContext.timezonePlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem :value="emptyTimezoneValue">
{{ $t('common.optional') }}
</SelectItem>
<SelectItem
v-for="timezoneOption in timezones"
:key="timezoneOption"
:value="timezoneOption"
>
{{ timezoneOption }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<TimezoneSelect
:model-value="value || emptyTimezoneValue"
:placeholder="$t('browserContext.timezonePlaceholder')"
allow-empty
:empty-label="$t('common.optional')"
@update:model-value="(val) => handleChange(val === emptyTimezoneValue ? '' : val)"
/>
</FormControl>
</FormItem>
</FormField>
@@ -249,12 +234,6 @@ import {
Label,
Separator,
Button,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
Switch,
} from '@memohai/ui'
import { toTypedSchema } from '@vee-validate/zod'
@@ -270,7 +249,8 @@ import { toast } from 'vue-sonner'
import { useDialogMutation } from '@/composables/useDialogMutation'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import { resolveApiErrorMessage } from '@/utils/api-error'
import { emptyTimezoneValue, timezones } from '@/utils/timezones'
import { emptyTimezoneValue } from '@/utils/timezones'
import TimezoneSelect from '@/components/timezone-select/index.vue'
const { t } = useI18n()
const { run } = useDialogMutation()
@@ -48,29 +48,11 @@
</div>
<div class="space-y-2">
<Label for="settings-timezone">{{ $t('settings.timezone') }}</Label>
<Select
<TimezoneSelect
:model-value="timezone"
:placeholder="$t('settings.timezonePlaceholder')"
@update:model-value="onTimezoneChange"
>
<SelectTrigger
id="settings-timezone"
class="w-full"
:aria-label="$t('settings.timezone')"
>
<SelectValue :placeholder="$t('settings.timezonePlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="timezoneOption in timezones"
:key="timezoneOption"
:value="timezoneOption"
>
{{ timezoneOption }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
/>
</div>
<div class="flex justify-end">
<Button
@@ -90,16 +72,10 @@ import {
Button,
Input,
Label,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Spinner,
} from '@memohai/ui'
import { timezones } from '@/utils/timezones'
import TimezoneSelect from '@/components/timezone-select/index.vue'
defineProps<{
displayUserId: string
+14
View File
@@ -5,3 +5,17 @@ export const timezones = typeof Intl.supportedValuesOf === 'function'
: fallbackTimezones
export const emptyTimezoneValue = '__empty_timezone__'
export function getUtcOffsetLabel(tz: string): string {
try {
const now = new Date()
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: tz,
timeZoneName: 'shortOffset',
}).formatToParts(now)
const offsetPart = parts.find(p => p.type === 'timeZoneName')
return offsetPart?.value ?? ''
} catch {
return ''
}
}
+1
View File
@@ -405,6 +405,7 @@ func (a *Agent) assembleTools(ctx context.Context, cfg RunConfig) ([]sdk.Tool, e
SupportsImageInput: cfg.SupportsImageInput,
IsSubagent: cfg.Identity.IsSubagent,
Skills: skillsMap,
TimezoneLocation: cfg.Identity.TimezoneLocation,
}
var allTools []sdk.Tool
+1 -1
View File
@@ -67,7 +67,7 @@ func (p *ContactsProvider) Tools(_ context.Context, session SessionContext) ([]s
"conversation_type": r.ConversationType,
"target": r.ReplyTarget,
"conversation_id": r.ConversationID,
"last_active": r.UpdatedAt.Format("2006-01-02T15:04:05Z"),
"last_active": sess.FormatTime(r.UpdatedAt),
}
if len(r.Metadata) > 0 {
if v, ok := r.Metadata["conversation_name"].(string); ok && v != "" {
+3 -3
View File
@@ -166,8 +166,8 @@ func (p *HistoryProvider) execListSessions(ctx context.Context, sess SessionCont
"platform": s.ChannelType,
"route_id": s.RouteID,
"conversation_type": s.RouteConversationType,
"last_active": s.UpdatedAt.Format(time.RFC3339),
"created_at": s.CreatedAt.Format(time.RFC3339),
"last_active": sess.FormatTime(s.UpdatedAt),
"created_at": sess.FormatTime(s.CreatedAt),
}
if m := s.RouteMetadata; len(m) > 0 {
@@ -261,7 +261,7 @@ func (p *HistoryProvider) execSearchMessages(ctx context.Context, sess SessionCo
"session_id": row.SessionID.String(),
"role": row.Role,
"text": text,
"created_at": row.CreatedAt.Time.Format(time.RFC3339),
"created_at": sess.FormatTime(row.CreatedAt.Time),
}
if dbpkg.TextToString(row.Platform) != "" {
entry["platform"] = dbpkg.TextToString(row.Platform)
+10
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"strings"
"time"
sdk "github.com/memohai/twilight-ai/sdk"
)
@@ -28,6 +29,15 @@ type SessionContext struct {
SupportsImageInput bool
IsSubagent bool
Skills map[string]SkillDetail
TimezoneLocation *time.Location
}
// FormatTime formats a time.Time using the session timezone (falls back to UTC).
func (s SessionContext) FormatTime(t time.Time) string {
if s.TimezoneLocation != nil {
t = t.In(s.TimezoneLocation)
}
return t.Format(time.RFC3339)
}
// ToolProvider supplies a set of tools for the agent.
+1 -1
View File
@@ -158,7 +158,7 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
return resolvedContext{}, err
}
loopDetectionEnabled := r.loadBotLoopDetectionEnabled(ctx, req.BotID)
userTimezoneName, userClockLocation := r.resolveUserTimezone(ctx, req.UserID)
userTimezoneName, userClockLocation := r.resolveTimezone(ctx, req.BotID, req.UserID)
var chatSettings conversation.Settings
if r.conversationSvc != nil {
@@ -69,12 +69,14 @@ func (r *Resolver) storeMemory(ctx context.Context, req conversation.ChatRequest
if p == nil {
return
}
_, tzLoc := r.resolveTimezone(ctx, req.BotID, req.UserID)
if err := p.OnAfterChat(ctx, memprovider.AfterChatRequest{
BotID: botID,
Messages: memMsgs,
UserID: strings.TrimSpace(req.UserID),
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
DisplayName: r.resolveDisplayName(ctx, req),
TimezoneLocation: tzLoc,
}); err != nil {
r.logger.Warn("memory provider OnAfterChat failed", slog.String("bot_id", botID), slog.Any("error", err))
}
+71 -20
View File
@@ -6,39 +6,90 @@ import (
"strings"
"time"
"github.com/memohai/memoh/internal/db"
"github.com/memohai/memoh/internal/timezone"
)
func (r *Resolver) resolveUserTimezone(ctx context.Context, userID string) (string, *time.Location) {
fallbackLocation := r.clockLocation
fallbackName := timezone.DefaultName
if fallbackLocation != nil {
fallbackName = fallbackLocation.String()
} else {
fallbackLocation = timezone.MustResolve(fallbackName)
// resolveTimezone resolves the effective timezone for a request.
// Priority: bot timezone > user timezone > system default.
func (r *Resolver) resolveTimezone(ctx context.Context, botID, userID string) (string, *time.Location) {
fallbackName, fallbackLocation := r.systemTimezoneDefaults()
// 1. Try bot timezone first.
if name, loc, ok := r.loadBotTimezone(ctx, botID); ok {
return name, loc
}
if r.accountService == nil {
return fallbackName, fallbackLocation
// 2. Fall back to user timezone.
if name, loc, ok := r.loadUserTimezone(ctx, userID); ok {
return name, loc
}
account, err := r.accountService.Get(ctx, strings.TrimSpace(userID))
return fallbackName, fallbackLocation
}
func (r *Resolver) systemTimezoneDefaults() (string, *time.Location) {
if r.clockLocation != nil {
return r.clockLocation.String(), r.clockLocation
}
return timezone.DefaultName, timezone.MustResolve(timezone.DefaultName)
}
func (r *Resolver) loadBotTimezone(ctx context.Context, botID string) (string, *time.Location, bool) {
if r.queries == nil || strings.TrimSpace(botID) == "" {
return "", nil, false
}
botUUID, err := db.ParseUUID(botID)
if err != nil {
return fallbackName, fallbackLocation
return "", nil, false
}
if strings.TrimSpace(account.Timezone) == "" {
return fallbackName, fallbackLocation
row, err := r.queries.GetBotByID(ctx, botUUID)
if err != nil {
return "", nil, false
}
loc, name, err := timezone.Resolve(account.Timezone)
tz := ""
if row.Timezone.Valid {
tz = strings.TrimSpace(row.Timezone.String)
}
if tz == "" {
return "", nil, false
}
loc, name, err := timezone.Resolve(tz)
if err != nil {
if r.logger != nil {
r.logger.Warn(
"resolve user timezone failed",
slog.String("user_id", userID),
slog.String("timezone", account.Timezone),
r.logger.Warn("resolve bot timezone failed",
slog.String("bot_id", botID),
slog.String("timezone", tz),
slog.Any("error", err),
)
}
return fallbackName, fallbackLocation
return "", nil, false
}
return name, loc
return name, loc, true
}
func (r *Resolver) loadUserTimezone(ctx context.Context, userID string) (string, *time.Location, bool) {
if r.accountService == nil || strings.TrimSpace(userID) == "" {
return "", nil, false
}
account, err := r.accountService.Get(ctx, strings.TrimSpace(userID))
if err != nil {
return "", nil, false
}
tz := strings.TrimSpace(account.Timezone)
if tz == "" {
return "", nil, false
}
loc, name, err := timezone.Resolve(tz)
if err != nil {
if r.logger != nil {
r.logger.Warn("resolve user timezone failed",
slog.String("user_id", userID),
slog.String("timezone", tz),
slog.Any("error", err),
)
}
return "", nil, false
}
return name, loc, true
}
@@ -39,7 +39,8 @@ func runFormation(ctx context.Context, logger *slog.Logger, llm adapters.LLM, ru
result := formationResult{}
extracted, err := llm.Extract(ctx, adapters.ExtractRequest{
Messages: req.Messages,
Messages: req.Messages,
TimezoneLocation: req.TimezoneLocation,
})
if err != nil {
logger.Warn("memory formation: extract failed", slog.String("bot_id", botID), slog.Any("error", err))
+5 -3
View File
@@ -24,6 +24,7 @@ type AfterChatRequest struct {
UserID string
ChannelIdentityID string
DisplayName string
TimezoneLocation *time.Location
}
// LLM is the interface for LLM operations needed by memory service.
@@ -148,9 +149,10 @@ type DeleteResponse struct {
}
type ExtractRequest struct {
Messages []Message `json:"messages"`
Filters map[string]any `json:"filters,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Messages []Message `json:"messages"`
Filters map[string]any `json:"filters,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
TimezoneLocation *time.Location `json:"-"`
}
type ExtractResponse struct {
+5 -1
View File
@@ -79,7 +79,11 @@ func (c *Client) Extract(ctx context.Context, req adapters.ExtractRequest) (adap
return adapters.ExtractResponse{}, nil
}
systemPrompt := strings.ReplaceAll(agent.MemoryExtractPrompt, "{{today}}", time.Now().UTC().Format("2006-01-02"))
now := time.Now()
if req.TimezoneLocation != nil {
now = now.In(req.TimezoneLocation)
}
systemPrompt := strings.ReplaceAll(agent.MemoryExtractPrompt, "{{today}}", now.Format("2006-01-02"))
result, err := sdk.GenerateTextResult(ctx,
sdk.WithModel(c.model()),