Add LLM-generated text to outlaw apprehended notification (#5691)

This commit is contained in:
2026-01-29 17:35:54 -08:00
committed by GitHub
parent 9a7dd953f9
commit 3bf44093a5
19 changed files with 214 additions and 34 deletions
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Net.Eagle0.Eagle.Common;
using Net.Eagle0.Eagle.Views;
namespace eagle.Notifications.ARNNotifications {
@@ -10,26 +11,25 @@ namespace eagle.Notifications.ARNNotifications {
IGameModel currentModel) {
var details = notification.Details.OutlawApprehendedDetails;
var province = currentModel.Provinces[details.ProvinceId];
var factionName = currentModel.FactionName(details.FactionId);
var factionHead =
currentModel.Heroes[currentModel.MaybeDestroyedFaction(details.FactionId)
.FactionHeadId];
var apprehended = currentModel.Heroes[details.ApprehendedHeroId];
var apprehending = currentModel.Heroes[details.ApprehendingHeroId];
string noteTitle = "Outlaw Apprehended";
string textTemplate =
$"{factionName} apprehended the outlaw {{ApprehendedHero}} in {province.Name}.";
$"{{ApprehendingHero}} apprehended the outlaw {{ApprehendedHero}} in {province.Name}. The captured outlaw reflects:\n\n";
var heroPlaceholders = new Dictionary<string, (string nameTextId, string fallback)> {
{ "ApprehendedHero", (apprehended.NameTextId, "an outlaw") }
{ "ApprehendedHero", (apprehended.NameTextId, "an outlaw") },
{ "ApprehendingHero", (apprehending.NameTextId, "a hero") }
};
yield return new DynamicTextNotification(
yield return DynamicTextNotification.StreamingDynamicNotification(
title: noteTitle,
textTemplate: textTemplate,
heroPlaceholders: heroPlaceholders,
singleProvinceId: details.ProvinceId,
displayedHeroes: new List<HeroView> { factionHead, apprehended });
llmId: notification.LlmId,
provinceIds: new List<int> { details.ProvinceId },
displayedHeroes: new List<HeroView> { apprehending, apprehended });
}
}
}
@@ -127,6 +127,7 @@ message OutlawApprehendedDetails {
int32 faction_id = 1;
int32 province_id = 2;
int32 apprehended_hero_id = 3;
int32 apprehending_hero_id = 4;
}
message OutlawSpottedDetails {
@@ -112,6 +112,7 @@ message OutlawApprehendedEvent {
int32 faction_id = 1;
int32 province_id = 2;
int32 apprehended_hero_id = 3;
int32 apprehending_hero_id = 4;
}
message PrisonerExecutedEvent {
@@ -76,6 +76,7 @@ message GeneratedTextRequestDetails {
VassalRisesMessage vassal_rises_message = 36;
LowFoodWarningMessage low_food_warning_message = 37;
StarvationNotificationMessage starvation_notification_message = 38;
OutlawApprehendedMessage outlaw_apprehended_message = 39;
}
}
@@ -379,3 +380,10 @@ message StarvationNotificationMessage {
int32 faction_id = 3;
double loss_proportion = 4; // 0.0-0.2 (capped by MaxBattalionReductionForFoodShortage)
}
message OutlawApprehendedMessage {
int32 apprehended_hero_id = 1;
int32 apprehending_hero_id = 2;
int32 faction_id = 3;
int32 province_id = 4;
}
@@ -252,13 +252,15 @@ object ChronicleEventGenerator {
case NotificationDetails.OutlawApprehended(
factionId,
provinceId,
apprehendedHeroId
apprehendedHeroId,
apprehendingHeroId
) =>
OutlawApprehendedChronicleEvent(
date = date,
factionId = factionId,
provinceId = provinceId,
apprehendedHeroId = apprehendedHeroId
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId
)
case NotificationDetails.RansomPaid(
@@ -1,20 +1,23 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.eagle.{FactionId, HeroId}
import net.eagle0.eagle.{FactionId, GameId, HeroId}
import net.eagle0.eagle.library.actions.impl.common.ProtolessSimpleAction
import net.eagle0.eagle.library.settings.{ApprehendOutlawVigorCost, FactionBiasFromImprisonment}
import net.eagle0.eagle.library.util.EagleRequire.clientRequire
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, NotificationC, StatDelta}
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.action_result.NotificationDetails.OutlawApprehended
import net.eagle0.eagle.model.action_result.NotificationT
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.unaffiliated_hero.{RecruitmentInfo, UnaffiliatedHeroType}
object ApprehendOutlawCommand {
def make(
actingFactionId: FactionId,
gameId: GameId,
availableHeroIds: Vector[HeroId],
outlawHeroIds: Vector[HeroId],
actingHeroId: HeroId,
@@ -34,6 +37,7 @@ object ApprehendOutlawCommand {
ApprehendOutlawCommand(
factionId = actingFactionId,
gameId = gameId,
province = actingProvince,
actingHeroId = actingHeroId,
targetedHeroId = heroIdToApprehend
@@ -42,12 +46,38 @@ object ApprehendOutlawCommand {
private case class ApprehendOutlawCommand(
factionId: FactionId,
gameId: GameId,
province: ProvinceT,
actingHeroId: HeroId,
targetedHeroId: HeroId
) extends ProtolessSimpleAction {
override def immediateExecute: ActionResultT =
override def immediateExecute: ActionResultT = {
val requestId = s"outlaw_apprehended_${targetedHeroId}_${province.id}"
val llmRequest = LlmRequestT.OutlawApprehendedMessage(
requestId = requestId,
eagleGameId = gameId,
recipientFactionIds = Vector.empty, // All factions
apprehendedHeroId = targetedHeroId,
apprehendingHeroId = actingHeroId,
factionId = factionId,
provinceId = province.id
)
val notification = NotificationC(
details = OutlawApprehended(
factionId = factionId,
provinceId = province.id,
apprehendedHeroId = targetedHeroId,
apprehendingHeroId = actingHeroId
),
affectedHeroIds = Vector(targetedHeroId, actingHeroId),
affectedProvinceIds = Vector(province.id),
llm = NotificationT.Llm.Id(requestId),
deferred = true
)
ActionResultC(
actionResultType = ActionResultType.OutlawApprehended,
actingFactionId = Some(factionId),
@@ -79,18 +109,9 @@ object ApprehendOutlawCommand {
)
)
),
newNotifications = Vector(
NotificationC(
details = OutlawApprehended(
factionId = factionId,
provinceId = province.id,
apprehendedHeroId = targetedHeroId
),
affectedHeroIds = Vector(targetedHeroId),
affectedProvinceIds = Vector(province.id),
deferred = true
)
)
newNotifications = Vector(notification),
newGeneratedTextRequests = Vector(llmRequest)
)
}
}
}
@@ -108,6 +108,7 @@ class CommandFactory extends TCommandFactory {
case (ac: ApprehendOutlawAvailable, sc: ApprehendOutlawSelected) =>
ApprehendOutlawCommand.make(
actingFactionId = actingFactionId,
gameId = gameState.gameId,
actingProvince = gameState.provinces(ac.actingProvinceId),
availableHeroIds = ac.availableHeroIds,
outlawHeroIds = ac.outlaws.map(_.heroId),
@@ -630,6 +630,24 @@ scala_library(
],
)
scala_library(
name = "outlaw_apprehended_prompt_generator",
srcs = ["OutlawApprehendedPromptGenerator.scala"],
visibility = ["//visibility:public"],
deps = [
":faction_info_utilities",
":generator_utilities",
":hero_description_generator",
":hero_info_utilities",
":llm_prompt_generator",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
scala_library(
name = "suppress_beasts_prompt_generator",
srcs = ["SuppressBeastsPromptGenerator.scala"],
@@ -192,7 +192,8 @@ object ChronicleEventTextGenerator {
_,
factionId,
provinceId,
apprehendedHeroId
apprehendedHeroId,
_ /* apprehendingHeroId */
) =>
val faction = getFaction(factionId, gameState)
val province = gameState.provinces(provinceId)
@@ -0,0 +1,50 @@
package net.eagle0.eagle.library.actions.llm_prompt_generators
import net.eagle0.eagle.client_text.{ClientTextStore, TextGenerationResult}
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT
import net.eagle0.eagle.model.state.game_state.GameState
case class OutlawApprehendedPromptGenerator(
message: LlmRequestT.OutlawApprehendedMessage,
gameState: GameState,
clientTextStore: ClientTextStore
) extends LLMPromptGenerator {
private val apprehendedHero = HeroInfoUtilities.getHero(message.apprehendedHeroId, gameState)
private val apprehendingHero = HeroInfoUtilities.getHero(message.apprehendingHeroId, gameState)
override def generate: TextGenerationResult = for {
apprehendedDescription <- HeroDescriptionGenerator.description(
heroId = message.apprehendedHeroId,
gameState = gameState,
clientTextStore = clientTextStore
)
apprehendingDescription <- HeroDescriptionGenerator.description(
heroId = message.apprehendingHeroId,
gameState = gameState,
clientTextStore = clientTextStore
)
apprehendedName <- clientTextStore.getText(apprehendedHero.nameTextId)
apprehendingName <- clientTextStore.getText(apprehendingHero.nameTextId)
setup <- GeneratorUtilities.basicSetup(gameState, clientTextStore)
} yield {
val province = gameState.provinces(message.provinceId)
val faction = FactionInfoUtilities.getFaction(message.factionId, gameState)
val factionName = faction.name
s"""$setup
|
|$apprehendedDescription
|
|$apprehendingDescription
|
|$apprehendedName was an outlaw in ${province.name}, living outside the law.
|$apprehendingName, a hero of $factionName, has just apprehended $apprehendedName.
|$apprehendedName is now a prisoner.
|
|Write a short reaction (25-35 words) from $apprehendedName reflecting on being captured.
|The tone can range from defiant to resigned, bitter to philosophical - whatever fits their personality.
|They might curse their captor, reflect on their past choices, or contemplate their fate.
|
|${GeneratorUtilities.limitations(35)}""".stripMargin
}
}
@@ -111,7 +111,8 @@ object NotificationDetails {
case class OutlawApprehended(
factionId: FactionId,
provinceId: ProvinceId,
apprehendedHeroId: HeroId
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId
) extends NotificationDetails
case class OutlawSpotted(
@@ -456,4 +456,15 @@ enum LlmRequestT extends GeneratedTextRequestT:
factionId: FactionId,
lossProportion: Double
)
case OutlawApprehendedMessage(
requestId: String,
eagleGameId: GameId,
recipientFactionIds: Vector[FactionId] = Vector.empty,
alwaysGenerate: Boolean = false,
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId,
factionId: FactionId,
provinceId: ProvinceId
)
end LlmRequestT
@@ -80,7 +80,8 @@ case class OutlawApprehendedChronicleEvent(
date: Date,
factionId: FactionId,
provinceId: ProvinceId,
apprehendedHeroId: HeroId
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId
) extends ChronicleEvent
case class PrisonerExecutedChronicleEvent(
@@ -230,12 +230,14 @@ object NotificationConverter {
case NotificationDetails.OutlawApprehended(
factionId,
provinceId,
apprehendedHeroId
apprehendedHeroId,
apprehendingHeroId
) =>
OutlawApprehendedDetails(
factionId = factionId,
provinceId = provinceId,
apprehendedHeroId = apprehendedHeroId
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId
)
case NotificationDetails.OutlawSpotted(outlawHeroId, provinceId) =>
OutlawSpottedDetails(
@@ -721,12 +723,14 @@ object NotificationConverter {
factionId: FactionId,
provinceId: ProvinceId,
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId,
_ /* unknownFields */
) =>
NotificationDetails.OutlawApprehended(
factionId = factionId,
provinceId = provinceId,
apprehendedHeroId = apprehendedHeroId
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId
)
case OutlawSpottedDetails(
@@ -23,6 +23,7 @@ import net.eagle0.eagle.internal.generated_text_request.{
InvitationResolutionMessage as InvitationResolutionMessageProto,
LowFoodWarningMessage as LowFoodWarningMessageProto,
NewFactionHeadMessage as NewFactionHeadMessageProto,
OutlawApprehendedMessage as OutlawApprehendedMessageProto,
PleaseRecruitMeMessage as PleaseRecruitMeMessageProto,
PrisonerExecutedMessage as PrisonerExecutedMessageProto,
PrisonerExiledMessage as PrisonerExiledMessageProto,
@@ -64,6 +65,7 @@ import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT.{
InvitationResolutionMessage,
LowFoodWarningMessage,
NewFactionHeadMessage,
OutlawApprehendedMessage,
PleaseRecruitMeMessage,
PrisonerExecutedMessage,
PrisonerExiledMessage,
@@ -765,6 +767,23 @@ object LlmRequestConverter {
factionId = factionId,
lossProportion = lossProportion
)
case OutlawApprehendedMessage(
_: String /* requestId */,
_: GameId /* eagleGameId */,
_: Vector[FactionId] /* recipientFactionIds */,
_: Boolean /* alwaysGenerate */,
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId,
factionId: FactionId,
provinceId: ProvinceId
) =>
OutlawApprehendedMessageProto(
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId,
factionId = factionId,
provinceId = provinceId
)
}
)
@@ -1468,6 +1487,24 @@ object LlmRequestConverter {
lossProportion = lossProportion
)
case OutlawApprehendedMessageProto(
apprehendedHeroId,
apprehendingHeroId,
factionId,
provinceId,
_ /* unknownFields */
) =>
OutlawApprehendedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId,
factionId = factionId,
provinceId = provinceId
)
case GeneratedTextRequestDetails.Empty =>
throw new IllegalArgumentException("Empty GeneratedTextRequestDetails")
}
@@ -177,12 +177,14 @@ object ChronicleEventConverter {
_: Date /* date */,
factionId: FactionId,
provinceId: ProvinceId,
apprehendedHeroId: HeroId
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId
) =>
OutlawApprehendedEventProto(
factionId = factionId,
provinceId = provinceId,
apprehendedHeroId = apprehendedHeroId
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId
)
case PrisonerExecutedChronicleEvent(
_: Date /* date */,
@@ -480,13 +482,15 @@ object ChronicleEventConverter {
factionId: FactionId,
provinceId: ProvinceId,
apprehendedHeroId: HeroId,
apprehendingHeroId: HeroId,
_ /* unknownFields */
) =>
OutlawApprehendedChronicleEvent(
date = DateConverter.fromProto(eventForChronicleProto.date),
factionId = factionId,
provinceId = provinceId,
apprehendedHeroId = apprehendedHeroId
apprehendedHeroId = apprehendedHeroId,
apprehendingHeroId = apprehendingHeroId
)
case PrisonerExecutedEventProto(
@@ -361,6 +361,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:llm_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:low_food_warning_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:new_faction_head_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:outlaw_apprehended_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:please_recruit_me_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:prisoner_executed_prompt_generator",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:prisoner_exiled_prompt_generator",
@@ -40,6 +40,7 @@ import net.eagle0.eagle.library.actions.llm_prompt_generators.{
LLMPromptGenerator,
LowFoodWarningPromptGenerator,
NewFactionHeadPromptGenerator,
OutlawApprehendedPromptGenerator,
PleaseRecruitMePromptGenerator,
PrisonerExecutedPromptGenerator,
PrisonerExiledPromptGenerator,
@@ -545,6 +546,13 @@ class LlmResolver(updateReceiver: LlmUpdateQueuingProxy, isGameValid: GameId =>
clientTextStore = clientTextStore
)
case outlawApprehendedMessage: LlmRequestT.OutlawApprehendedMessage =>
OutlawApprehendedPromptGenerator(
message = outlawApprehendedMessage,
gameState = gameState,
clientTextStore = clientTextStore
)
// Handle FixedHeroName and GeneratedHeroName - these are not LLM requests
case _: FixedHeroName =>
throw new EagleInternalException(
@@ -54,11 +54,13 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
private val heroIdToApprehend = 20
private val actingHeroId = 3
private val gameId = 123L
"make" should "throw if the selected actor was not in available" in {
val ex = the[EagleClientException] thrownBy
ApprehendOutlawCommand.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = 6,
@@ -72,6 +74,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val ex = the[EagleClientException] thrownBy
ApprehendOutlawCommand.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,
@@ -86,6 +89,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val result = ApprehendOutlawCommand
.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,
@@ -103,6 +107,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val result = ApprehendOutlawCommand
.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,
@@ -121,6 +126,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val result = ApprehendOutlawCommand
.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,
@@ -139,6 +145,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val result = ApprehendOutlawCommand
.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,
@@ -166,6 +173,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val result = ApprehendOutlawCommand
.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,
@@ -180,6 +188,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
oad.factionId shouldBe fid
oad.provinceId shouldBe pid
oad.apprehendedHeroId shouldBe 20
oad.apprehendingHeroId shouldBe actingHeroId
}
}
}
@@ -188,6 +197,7 @@ class ApprehendOutlawCommandTest extends AnyFlatSpec with Matchers with BeforeAn
val result = ApprehendOutlawCommand
.make(
actingFactionId = fid,
gameId = gameId,
availableHeroIds = availableHeroIds,
outlawHeroIds = outlawHeroIds,
actingHeroId = actingHeroId,