mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 13:54:02 +00:00
111 lines
3.5 KiB
TypeScript
111 lines
3.5 KiB
TypeScript
import { instantFromEpochSeconds, nowInstant, Temporal, type Instant } from '@household/domain'
|
||
import type { TopicMessageHistoryRecord, TopicMessageHistoryRepository } from '@household/ports'
|
||
|
||
export interface TopicHistoryTurn {
|
||
role: 'user' | 'assistant'
|
||
speaker: string
|
||
text: string
|
||
threadId: string | null
|
||
}
|
||
|
||
const MEMORY_LOOKUP_PATTERN =
|
||
/\b(?:do you remember|remember|what were we talking about|what did we say today)\b|(?:^|[^\p{L}])(?:помнишь|ты\s+помнишь|что\s+мы\s+сегодня\s+обсуждали|о\s+чем\s+мы\s+говорили)(?=$|[^\p{L}])/iu
|
||
|
||
export function shouldLoadExpandedChatHistory(text: string): boolean {
|
||
return MEMORY_LOOKUP_PATTERN.test(text.trim())
|
||
}
|
||
|
||
export function startOfCurrentDayInTimezone(
|
||
timezone: string,
|
||
referenceInstant = nowInstant()
|
||
): Instant {
|
||
const zoned = referenceInstant.toZonedDateTimeISO(timezone)
|
||
const startOfDay = Temporal.ZonedDateTime.from({
|
||
timeZone: timezone,
|
||
year: zoned.year,
|
||
month: zoned.month,
|
||
day: zoned.day,
|
||
hour: 0,
|
||
minute: 0,
|
||
second: 0,
|
||
millisecond: 0,
|
||
microsecond: 0,
|
||
nanosecond: 0
|
||
})
|
||
|
||
return startOfDay.toInstant()
|
||
}
|
||
|
||
export function historyRecordToTurn(record: TopicMessageHistoryRecord): TopicHistoryTurn {
|
||
return {
|
||
role: record.isBot ? 'assistant' : 'user',
|
||
speaker: record.senderDisplayName ?? (record.isBot ? 'Kojori Bot' : 'Unknown'),
|
||
text: record.rawText.trim(),
|
||
threadId: record.telegramThreadId
|
||
}
|
||
}
|
||
|
||
export function telegramMessageIdFromMessage(
|
||
message: { message_id?: number } | null | undefined
|
||
): string | null {
|
||
return typeof message?.message_id === 'number' ? message.message_id.toString() : null
|
||
}
|
||
|
||
export function telegramMessageSentAtFromMessage(
|
||
message: { date?: number } | null | undefined
|
||
): Instant | null {
|
||
return typeof message?.date === 'number' ? instantFromEpochSeconds(message.date) : null
|
||
}
|
||
|
||
export async function persistTopicHistoryMessage(input: {
|
||
repository: TopicMessageHistoryRepository | undefined
|
||
householdId: string
|
||
telegramChatId: string
|
||
telegramThreadId: string | null
|
||
telegramMessageId: string | null
|
||
telegramUpdateId: string | null
|
||
senderTelegramUserId: string | null
|
||
senderDisplayName: string | null
|
||
isBot: boolean
|
||
rawText: string
|
||
messageSentAt: Instant | null
|
||
}) {
|
||
const normalizedText = input.rawText.trim()
|
||
if (!input.repository || normalizedText.length === 0) {
|
||
return
|
||
}
|
||
|
||
await input.repository.saveMessage({
|
||
householdId: input.householdId,
|
||
telegramChatId: input.telegramChatId,
|
||
telegramThreadId: input.telegramThreadId,
|
||
telegramMessageId: input.telegramMessageId,
|
||
telegramUpdateId: input.telegramUpdateId,
|
||
senderTelegramUserId: input.senderTelegramUserId,
|
||
senderDisplayName: input.senderDisplayName,
|
||
isBot: input.isBot,
|
||
rawText: normalizedText,
|
||
messageSentAt: input.messageSentAt
|
||
})
|
||
}
|
||
|
||
export function formatThreadHistory(turns: readonly TopicHistoryTurn[]): string | null {
|
||
const lines = turns
|
||
.map((turn) => `${turn.speaker} (${turn.role}): ${turn.text}`)
|
||
.filter((line) => line.trim().length > 0)
|
||
|
||
return lines.length > 0 ? lines.join('\n') : null
|
||
}
|
||
|
||
export function formatSameDayChatHistory(turns: readonly TopicHistoryTurn[]): string | null {
|
||
const lines = turns
|
||
.map((turn) =>
|
||
turn.threadId
|
||
? `[thread ${turn.threadId}] ${turn.speaker} (${turn.role}): ${turn.text}`
|
||
: `${turn.speaker} (${turn.role}): ${turn.text}`
|
||
)
|
||
.filter((line) => line.trim().length > 0)
|
||
|
||
return lines.length > 0 ? lines.join('\n') : null
|
||
}
|