feat(bot): add configurable household assistant behavior

This commit is contained in:
2026-03-12 03:22:43 +04:00
parent 146f5294f4
commit 4e7400e908
22 changed files with 4127 additions and 96 deletions

View File

@@ -12,6 +12,7 @@ import {
HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES,
HOUSEHOLD_PAYMENT_BALANCE_ADJUSTMENT_POLICIES,
HOUSEHOLD_TOPIC_ROLES,
type HouseholdAssistantConfigRecord,
type HouseholdMemberAbsencePolicy,
type HouseholdMemberAbsencePolicyRecord,
type HouseholdBillingSettingsRecord,
@@ -245,6 +246,18 @@ function toHouseholdBillingSettingsRecord(row: {
}
}
function toHouseholdAssistantConfigRecord(row: {
householdId: string
assistantContext: string | null
assistantTone: string | null
}): HouseholdAssistantConfigRecord {
return {
householdId: row.householdId,
assistantContext: row.assistantContext,
assistantTone: row.assistantTone
}
}
function toHouseholdUtilityCategoryRecord(row: {
id: string
householdId: string
@@ -957,6 +970,25 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
return toHouseholdBillingSettingsRecord(row)
},
async getHouseholdAssistantConfig(householdId) {
const rows = await db
.select({
householdId: schema.households.id,
assistantContext: schema.households.assistantContext,
assistantTone: schema.households.assistantTone
})
.from(schema.households)
.where(eq(schema.households.id, householdId))
.limit(1)
const row = rows[0]
if (!row) {
throw new Error('Failed to load household assistant config')
}
return toHouseholdAssistantConfigRecord(row)
},
async updateHouseholdBillingSettings(input) {
await ensureBillingSettings(input.householdId)
@@ -1033,6 +1065,36 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
return toHouseholdBillingSettingsRecord(row)
},
async updateHouseholdAssistantConfig(input) {
const rows = await db
.update(schema.households)
.set({
...(input.assistantContext !== undefined
? {
assistantContext: input.assistantContext
}
: {}),
...(input.assistantTone !== undefined
? {
assistantTone: input.assistantTone
}
: {})
})
.where(eq(schema.households.id, input.householdId))
.returning({
householdId: schema.households.id,
assistantContext: schema.households.assistantContext,
assistantTone: schema.households.assistantTone
})
const row = rows[0]
if (!row) {
throw new Error('Failed to update household assistant config')
}
return toHouseholdAssistantConfigRecord(row)
},
async listHouseholdUtilityCategories(householdId) {
await ensureUtilityCategories(householdId)

View File

@@ -172,6 +172,16 @@ function repository(): HouseholdConfigurationRepository {
utilitiesReminderDay: input.utilitiesReminderDay ?? 3,
timezone: input.timezone ?? 'Asia/Tbilisi'
}),
getHouseholdAssistantConfig: async (householdId) => ({
householdId,
assistantContext: 'House in Kojori',
assistantTone: 'Playful'
}),
updateHouseholdAssistantConfig: async (input) => ({
householdId: input.householdId,
assistantContext: input.assistantContext ?? 'House in Kojori',
assistantTone: input.assistantTone ?? 'Playful'
}),
listHouseholdUtilityCategories: async () => [],
upsertHouseholdUtilityCategory: async (input) => ({
id: input.slug ?? 'utility-category-1',
@@ -269,6 +279,11 @@ describe('createMiniAppAdminService', () => {
utilitiesReminderDay: 3,
timezone: 'Asia/Tbilisi'
},
assistantConfig: {
householdId: 'household-1',
assistantContext: 'House in Kojori',
assistantTone: 'Playful'
},
topics: [
{
householdId: 'household-1',
@@ -322,6 +337,11 @@ describe('createMiniAppAdminService', () => {
utilitiesDueDay: 5,
utilitiesReminderDay: 4,
timezone: 'Asia/Tbilisi'
},
assistantConfig: {
householdId: 'household-1',
assistantContext: 'House in Kojori',
assistantTone: 'Playful'
}
})
})

View File

@@ -1,4 +1,5 @@
import type {
HouseholdAssistantConfigRecord,
HouseholdBillingSettingsRecord,
HouseholdConfigurationRepository,
HouseholdMemberAbsencePolicy,
@@ -29,6 +30,7 @@ export interface MiniAppAdminService {
| {
status: 'ok'
settings: HouseholdBillingSettingsRecord
assistantConfig: HouseholdAssistantConfigRecord
categories: readonly HouseholdUtilityCategoryRecord[]
members: readonly HouseholdMemberRecord[]
memberAbsencePolicies: readonly HouseholdMemberAbsencePolicyRecord[]
@@ -51,10 +53,13 @@ export interface MiniAppAdminService {
utilitiesDueDay: number
utilitiesReminderDay: number
timezone: string
assistantContext?: string
assistantTone?: string
}): Promise<
| {
status: 'ok'
settings: HouseholdBillingSettingsRecord
assistantConfig: HouseholdAssistantConfigRecord
}
| {
status: 'rejected'
@@ -210,6 +215,34 @@ function normalizeDisplayName(raw: string): string | null {
return trimmed.replace(/\s+/g, ' ')
}
function defaultAssistantConfig(householdId: string): HouseholdAssistantConfigRecord {
return {
householdId,
assistantContext: null,
assistantTone: null
}
}
function normalizeAssistantText(
raw: string | undefined,
maxLength: number
): string | null | undefined {
if (raw === undefined) {
return undefined
}
const trimmed = raw.trim()
if (trimmed.length === 0) {
return null
}
if (trimmed.length > maxLength) {
return null
}
return trimmed
}
export function createMiniAppAdminService(
repository: HouseholdConfigurationRepository
): MiniAppAdminService {
@@ -222,17 +255,22 @@ export function createMiniAppAdminService(
}
}
const [settings, categories, members, memberAbsencePolicies, topics] = await Promise.all([
repository.getHouseholdBillingSettings(input.householdId),
repository.listHouseholdUtilityCategories(input.householdId),
repository.listHouseholdMembers(input.householdId),
repository.listHouseholdMemberAbsencePolicies(input.householdId),
repository.listHouseholdTopicBindings(input.householdId)
])
const [settings, assistantConfig, categories, members, memberAbsencePolicies, topics] =
await Promise.all([
repository.getHouseholdBillingSettings(input.householdId),
repository.getHouseholdAssistantConfig
? repository.getHouseholdAssistantConfig(input.householdId)
: Promise.resolve(defaultAssistantConfig(input.householdId)),
repository.listHouseholdUtilityCategories(input.householdId),
repository.listHouseholdMembers(input.householdId),
repository.listHouseholdMemberAbsencePolicies(input.householdId),
repository.listHouseholdTopicBindings(input.householdId)
])
return {
status: 'ok',
settings,
assistantConfig,
categories,
members,
memberAbsencePolicies,
@@ -263,6 +301,23 @@ export function createMiniAppAdminService(
}
}
const assistantContext = normalizeAssistantText(input.assistantContext, 1200)
const assistantTone = normalizeAssistantText(input.assistantTone, 160)
if (
(input.assistantContext !== undefined &&
assistantContext === null &&
input.assistantContext.trim().length > 0) ||
(input.assistantTone !== undefined &&
assistantTone === null &&
input.assistantTone.trim().length > 0)
) {
return {
status: 'rejected',
reason: 'invalid_settings'
}
}
let rentAmountMinor: bigint | null | undefined
let rentCurrency: CurrencyCode | undefined
const settlementCurrency = input.settlementCurrency
@@ -291,38 +346,65 @@ export function createMiniAppAdminService(
rentCurrency = parseCurrency(input.rentCurrency ?? 'USD')
}
const settings = await repository.updateHouseholdBillingSettings({
householdId: input.householdId,
...(settlementCurrency
? {
settlementCurrency
}
: {}),
...(paymentBalanceAdjustmentPolicy
? {
paymentBalanceAdjustmentPolicy
}
: {}),
...(rentAmountMinor !== undefined
? {
rentAmountMinor
}
: {}),
...(rentCurrency
? {
rentCurrency
}
: {}),
rentDueDay: input.rentDueDay,
rentWarningDay: input.rentWarningDay,
utilitiesDueDay: input.utilitiesDueDay,
utilitiesReminderDay: input.utilitiesReminderDay,
timezone: input.timezone.trim()
})
const shouldUpdateAssistantConfig =
assistantContext !== undefined || assistantTone !== undefined
const [settings, nextAssistantConfig] = await Promise.all([
repository.updateHouseholdBillingSettings({
householdId: input.householdId,
...(settlementCurrency
? {
settlementCurrency
}
: {}),
...(paymentBalanceAdjustmentPolicy
? {
paymentBalanceAdjustmentPolicy
}
: {}),
...(rentAmountMinor !== undefined
? {
rentAmountMinor
}
: {}),
...(rentCurrency
? {
rentCurrency
}
: {}),
rentDueDay: input.rentDueDay,
rentWarningDay: input.rentWarningDay,
utilitiesDueDay: input.utilitiesDueDay,
utilitiesReminderDay: input.utilitiesReminderDay,
timezone: input.timezone.trim()
}),
repository.updateHouseholdAssistantConfig && shouldUpdateAssistantConfig
? repository.updateHouseholdAssistantConfig({
householdId: input.householdId,
...(assistantContext !== undefined
? {
assistantContext
}
: {}),
...(assistantTone !== undefined
? {
assistantTone
}
: {})
})
: repository.getHouseholdAssistantConfig
? repository.getHouseholdAssistantConfig(input.householdId)
: Promise.resolve({
householdId: input.householdId,
assistantContext: assistantContext ?? null,
assistantTone: assistantTone ?? null
})
])
return {
status: 'ok',
settings
settings,
assistantConfig: nextAssistantConfig
}
},

View File

@@ -18,6 +18,7 @@
"0014_empty_risque.sql": "6dd4aba0f84d43bc86afbd04cad3b1055ecac03bd80ad6fd510bef1550d10335",
"0015_white_owl.sql": "a9dec4c536c660d7eb0fcea42a3bedb1301408551977d098dff8324d7d5b26bd",
"0016_equal_susan_delgado.sql": "1698bf0516d16d2d7929dcb1bd2bb76d5a629eaba3d0bb2533c1ae926408de7a",
"0017_gigantic_selene.sql": "232d61b979675ddb97c9d69d14406dc15dd095ee6a332d3fa71d10416204fade"
"0017_gigantic_selene.sql": "232d61b979675ddb97c9d69d14406dc15dd095ee6a332d3fa71d10416204fade",
"0018_nimble_kojori.sql": "818738e729119c6de8049dcfca562926a5dc6e321ecbbf9cf38e02bc70b5a0dc"
}
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE "households" ADD COLUMN "assistant_context" text;
ALTER TABLE "households" ADD COLUMN "assistant_tone" text;

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,13 @@
"when": 1773226133315,
"tag": "0017_gigantic_selene",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1773252000000,
"tag": "0018_nimble_kojori",
"breakpoints": true
}
]
}

View File

@@ -16,6 +16,8 @@ export const households = pgTable('households', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
defaultLocale: text('default_locale').default('ru').notNull(),
assistantContext: text('assistant_context'),
assistantTone: text('assistant_tone'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
})

View File

@@ -86,6 +86,12 @@ export interface HouseholdBillingSettingsRecord {
timezone: string
}
export interface HouseholdAssistantConfigRecord {
householdId: string
assistantContext: string | null
assistantTone: string | null
}
export interface HouseholdUtilityCategoryRecord {
id: string
householdId: string
@@ -166,6 +172,7 @@ export interface HouseholdConfigurationRepository {
): Promise<HouseholdMemberRecord | null>
listHouseholdMembers(householdId: string): Promise<readonly HouseholdMemberRecord[]>
getHouseholdBillingSettings(householdId: string): Promise<HouseholdBillingSettingsRecord>
getHouseholdAssistantConfig?(householdId: string): Promise<HouseholdAssistantConfigRecord>
updateHouseholdBillingSettings(input: {
householdId: string
settlementCurrency?: CurrencyCode
@@ -178,6 +185,11 @@ export interface HouseholdConfigurationRepository {
utilitiesReminderDay?: number
timezone?: string
}): Promise<HouseholdBillingSettingsRecord>
updateHouseholdAssistantConfig?(input: {
householdId: string
assistantContext?: string | null
assistantTone?: string | null
}): Promise<HouseholdAssistantConfigRecord>
listHouseholdUtilityCategories(
householdId: string
): Promise<readonly HouseholdUtilityCategoryRecord[]>

View File

@@ -19,6 +19,7 @@ export {
HOUSEHOLD_TOPIC_ROLES,
type HouseholdMemberAbsencePolicy,
type HouseholdMemberAbsencePolicyRecord,
type HouseholdAssistantConfigRecord,
type HouseholdPaymentBalanceAdjustmentPolicy,
type HouseholdConfigurationRepository,
type HouseholdBillingSettingsRecord,