Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ data class ScheduleOptionDraft(
val slots: List<ScheduleSlotDraft>,
val satisfactionByUser: List<SatisfactionDraft>,
val llmProvider: String,
val llmAttemptedProvider: String,
val llmLatencyMs: Long?,
val fallbackUsed: Boolean,
val llmFallbackReason: String?,
)

data class OptionContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ class ConsensusService(
)
}

val llmRefined = llmService.refineScheduleOption(
val llmAttempt = llmService.refineScheduleOption(
optionType = optionType,
label = label,
summary = summary,
Expand All @@ -498,6 +498,7 @@ class ConsensusService(
members = members.map { MemberRef(it.userId, it.nickname) },
slotPlan = shortlistedPerSlot,
)
val llmRefined = llmAttempt.result

val finalSummary = llmRefined?.summary ?: summary
val placesById = places.associateBy { it.id }
Expand Down Expand Up @@ -531,7 +532,7 @@ class ConsensusService(
val groupSatisfaction = maxOf(threshold, satisfactionByUser.minOf { it.score })

logger.info {
"schedule_option optionType=$optionType roomId=${context.roomId} provider=${llmRefined?.provider ?: DETERMINISTIC_PROVIDER} latencyMs=${llmRefined?.latencyMs ?: 0} fallbackUsed=${llmRefined == null} groupSatisfaction=$groupSatisfaction"
"schedule_option optionType=$optionType roomId=${context.roomId} provider=${llmRefined?.provider ?: DETERMINISTIC_PROVIDER} attemptedProvider=${llmAttempt.attemptedProvider} latencyMs=${llmAttempt.latencyMs ?: 0} fallbackUsed=${llmAttempt.fallbackUsed} fallbackReason=${llmAttempt.fallbackReason?.code ?: "none"} groupSatisfaction=$groupSatisfaction"
}

return ScheduleOptionDraft(
Expand All @@ -542,8 +543,10 @@ class ConsensusService(
slots = finalSlots,
satisfactionByUser = satisfactionByUser,
llmProvider = llmRefined?.provider ?: DETERMINISTIC_PROVIDER,
llmLatencyMs = llmRefined?.latencyMs,
fallbackUsed = llmRefined == null,
llmAttemptedProvider = llmAttempt.attemptedProvider,
llmLatencyMs = llmAttempt.latencyMs,
fallbackUsed = llmAttempt.fallbackUsed,
llmFallbackReason = llmAttempt.fallbackReason?.code,
)
}

Expand Down
19 changes: 18 additions & 1 deletion src/main/kotlin/com/tripsync/application/consensus/LlmService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,30 @@ class LlmService(
val reason: String,
)

enum class FallbackReason(val code: String) {
API_KEY_MISSING("api_key_missing"),
API_HTTP_ERROR("api_http_error"),
API_CALL_FAILED("api_call_failed"),
RESPONSE_PARSE_FAILED("response_parse_failed"),
RESPONSE_SCHEMA_INVALID("response_schema_invalid"),
}

data class RefinementResult(
val summary: String,
val provider: String,
val latencyMs: Long,
val slots: List<RefinedSlot>,
)

data class RefinementAttempt(
val result: RefinementResult?,
val attemptedProvider: String,
val latencyMs: Long?,
val fallbackUsed: Boolean,
val fallbackReason: FallbackReason?,
val failureDetail: String? = null,
)

suspend fun refineScheduleOption(
optionType: ScheduleOptionType,
label: String,
Expand All @@ -31,7 +48,7 @@ class LlmService(
priorityAxes: List<com.tripsync.domain.enums.ScoreAxis>,
members: List<ConsensusService.MemberRef>,
slotPlan: List<ConsensusService.SlotShortlist>,
): RefinementResult? {
): RefinementAttempt {
return openAiClient.refineSchedule(
optionType = optionType,
label = label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ class ScheduleGenerationPersistenceService(
"endTime" to dto.endTime,
"tripStartDate" to (dto.tripStartDate ?: dto.tripDate),
"tripEndDate" to (dto.tripEndDate ?: dto.tripDate),
"llm" to mapOf(
"provider" to option.llmProvider,
"attemptedProvider" to option.llmAttemptedProvider,
"latencyMs" to option.llmLatencyMs,
"fallbackUsed" to option.fallbackUsed,
"fallbackReason" to option.llmFallbackReason,
),
),
summary = option.summary,
groupSatisfaction = option.groupSatisfaction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ class ScheduleResponseMapper(
"groupSatisfaction" to option.groupSatisfaction,
"personaValidation" to personaValidation,
"llmProvider" to option.llmProvider,
"llmAttemptedProvider" to option.llmAttemptedProvider,
"llmLatencyMs" to option.llmLatencyMs,
"fallbackUsed" to option.fallbackUsed,
"llmFallbackReason" to option.llmFallbackReason,
"slots" to option.slots.sortedBy { it.orderIndex }.map { slot ->
val place = placesById[slot.placeId]
mapOf(
Expand Down Expand Up @@ -55,6 +57,7 @@ class ScheduleResponseMapper(
fun formatStoredSchedule(schedule: Schedule): Map<String, Any?> {
val memberNicknames = roomMemberProfileRepository.findAllByRoomIdAndDelYn(schedule.room.id, YnFlag.N)
.associate { it.user.id to it.user.nickname }
val llmMetadata = formatLlmMetadata(schedule)
return mapOf(
"id" to schedule.id,
"roomId" to schedule.room.id,
Expand All @@ -66,7 +69,12 @@ class ScheduleResponseMapper(
"groupSatisfaction" to schedule.groupSatisfaction,
"summary" to (schedule.summary ?: ""),
"personaValidation" to schedule.personaValidation,
"slots" to schedule.slots.filter { it.delYn == YnFlag.N }.sortedBy { it.orderIndex }.map { slot ->
"llmProvider" to llmMetadata["provider"],
"llmAttemptedProvider" to llmMetadata["attemptedProvider"],
"llmLatencyMs" to llmMetadata["latencyMs"],
"fallbackUsed" to llmMetadata["fallbackUsed"],
"llmFallbackReason" to llmMetadata["fallbackReason"],
"slots" to schedule.slots.filter { it.delYn == YnFlag.N }.sortedBy { slot -> slot.orderIndex }.map { slot ->
mapOf(
"slotId" to slot.id,
"orderIndex" to slot.orderIndex,
Expand All @@ -91,6 +99,37 @@ class ScheduleResponseMapper(
)
}

fun formatPublicShareSchedule(schedule: Schedule): Map<String, Any?> {
val publicKeys = setOf(
"id",
"roomId",
"destination",
"tripDate",
"version",
"optionType",
"isConfirmed",
"groupSatisfaction",
"summary",
"personaValidation",
"slots",
"satisfactionByUser",
)
return formatStoredSchedule(schedule).filterKeys { it in publicKeys }
}

@Suppress("UNCHECKED_CAST")
private fun formatLlmMetadata(schedule: Schedule): Map<String, Any?> {
val metadata = schedule.generationInput["llm"] as? Map<String, Any?> ?: emptyMap()
val provider = schedule.llmProvider ?: metadata["provider"]
return mapOf(
"provider" to provider,
"attemptedProvider" to (metadata["attemptedProvider"] ?: provider),
"latencyMs" to metadata["latencyMs"],
"fallbackUsed" to (metadata["fallbackUsed"] ?: (provider == "deterministic-consensus")),
"fallbackReason" to metadata["fallbackReason"],
)
}

fun formatPlace(place: Place?, id: Long, name: String, address: String): Map<String, Any?> = mapOf(
"id" to id,
"name" to name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ class ScheduleService(
@Transactional(readOnly = true)
fun getPublicShareSchedule(scheduleId: Long): ApiResponse<Map<String, Any?>> {
val schedule = accessPolicy.getActiveSchedule(scheduleId)
return ApiResponse.ok(responseMapper.formatStoredSchedule(schedule))
return ApiResponse.ok(responseMapper.formatPublicShareSchedule(schedule))
}

private fun safePersonaValidationByType(
Expand Down
Loading
Loading