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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ tasks.register<Exec>("deployToSSH") {
remotePath
?: throw GradleException("REMOTE_PATH not set. Set it as environment variable or in local.properties")

commandLine("scp", localFile, "$user@$host:$path")
commandLine("scp", "-v", localFile, "$user@$host:$path")

doLast {
println("✅ Deployed plugin JAR to $user@$host:$path")
Expand Down
69 changes: 69 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Conversation Theme System - Implementation Plan

## Architecture

### Core abstractions (new package: `conversation/theme/`)

1. **`ConversationTheme`** - Abstract base class that defines a theme
- `name: String` - unique identifier (e.g., "chat", "violence")
- `displayName: String` - human-readable name
- `compatibleWith: Set<String>` - theme names this can stack with
- `onActivate(conversation: Conversation)` - callback when theme becomes active
- `onDeactivate(conversation: Conversation)` - callback when theme is removed
- `onMessage(conversation: Conversation, message: ConversationMessage)` - called on each message

2. **`ConversationThemeFactory`** - Interface for creating theme instances
- `fun create(): ConversationTheme`

3. **`ConversationThemeRegistry`** - Singleton registry
- `register(name: String, factory: ConversationThemeFactory)` - register a theme type
- `unregister(name: String)` - remove a theme type
- `create(name: String): ConversationTheme` - instantiate from factory
- `getRegisteredThemes(): Set<String>` - list available themes

4. **`ConversationThemeManager`** - Manages active themes per conversation (separate manager, observes conversations)
- `activateTheme(conversation: Conversation, themeName: String): Boolean` - activate a theme (checks compatibility with existing active themes)
- `deactivateTheme(conversation: Conversation, themeName: String): Boolean`
- `getActiveThemes(conversation: Conversation): List<ConversationTheme>`
- `hasTheme(conversation: Conversation, themeName: String): Boolean`
- `onConversationEnd(conversation: Conversation)` - cleanup
- `onMessage(conversation: Conversation, message: ConversationMessage)` - propagate to active themes

5. **`ConversationThemeData`** - Data class held within `Conversation`
- `activeThemeNames: List<String>` (read-only view of active theme names)
- Kept minimal - just state, no logic

### Example Themes

6. **`ChatTheme`** - Default theme, compatible with everything
7. **`ViolenceTheme`** - Activates on combat events, not compatible with "trade"

### Integration Points

- Add `themeData: ConversationThemeData` field to `Conversation`
- `ConversationThemeManager` initialized in `Story.onEnable()` and stored on plugin
- Theme manager listens to `ConversationStartEvent` to apply default "chat" theme
- Theme manager listens to conversation messages via `ConversationManager`

### File list
```
src/main/kotlin/com/canefe/story/conversation/theme/
├── ConversationTheme.kt # Abstract base
├── ConversationThemeFactory.kt # Factory interface
├── ConversationThemeRegistry.kt # Singleton registry
├── ConversationThemeManager.kt # Per-conversation manager
├── ConversationThemeData.kt # Data class for Conversation
├── ChatTheme.kt # Example: default chat
└── ViolenceTheme.kt # Example: violence/combat

Modified files:
- Conversation.kt # Add themeData field
- Story.kt # Initialize theme manager + register defaults
```

### Tests
```
src/test/kotlin/com/canefe/story/conversation/theme/
├── ConversationThemeRegistryTest.kt
├── ConversationThemeManagerTest.kt
```
18 changes: 18 additions & 0 deletions src/main/kotlin/com/canefe/story/Story.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.canefe.story.context.ContextExtractor
import com.canefe.story.conversation.ConversationManager
import com.canefe.story.conversation.ConversationMessage
import com.canefe.story.conversation.radiant.RadiantConversationService
import com.canefe.story.conversation.theme.*
import com.canefe.story.event.EventManager
import com.canefe.story.information.WorldInformationManager
import com.canefe.story.location.LocationManager
Expand Down Expand Up @@ -143,6 +144,14 @@ open class Story :

lateinit var voiceManager: VoiceManager

// Theme system
lateinit var themeRegistry: ConversationThemeRegistry
private set
lateinit var themeManager: ConversationThemeManager
private set
lateinit var themeAgent: com.canefe.story.conversation.theme.ConversationThemeAgent
private set

// NPC Name Aliasing System
lateinit var npcNameManager: com.canefe.story.npc.name.NPCNameManager
private set
Expand Down Expand Up @@ -289,6 +298,15 @@ open class Story :
npcNameManager = NPCNameManager(this)
npcNameResolver = NPCNameResolver(this)

// Initialize theme system
themeRegistry = ConversationThemeRegistry()
themeRegistry.register(ChatTheme.NAME) { ChatTheme() }
themeRegistry.register(ViolenceTheme.NAME) { ViolenceTheme() }
themeManager = ConversationThemeManager(themeRegistry)
themeAgent =
com.canefe.story.conversation.theme
.ConversationThemeAgent(this, themeManager, themeRegistry)

eventManager = EventManager(this)
eventManager.registerEvents()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,19 @@ class ConvListCommand(
npcNames: List<String>,
playerNames: List<String>,
): List<Component> {
// Build the prefix with conversation ID
// Build the prefix with conversation ID and active themes
val miniMessage = commandUtils.mm
val componentList = mutableListOf<Component>()
val prefix = miniMessage.deserialize("<gray>=====<green>[$id]</green>=====</gray>")

val conversation = commandUtils.conversationManager.getConversationById(id)
val activeThemes = conversation?.themeData?.activeThemeNames ?: emptyList()
val themeSuffix =
if (activeThemes.isNotEmpty()) {
" <dark_gray>[<light_purple>${activeThemes.joinToString(", ")}</light_purple>]</dark_gray>"
} else {
""
}
val prefix = miniMessage.deserialize("<gray>=====<green>[$id]</green>=====</gray>$themeSuffix")

// Append clickable NPC names
val clickableNpcNames = createClickableNpcNames(id, npcNames)
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/com/canefe/story/config/ConfigService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class ConfigService(
false // Whether to enable dialogue path selection for DMs
var delayedPlayerMessageProcessing: Boolean =
false // Whether to delay player message processing like /g command
var summarizationThreshold: Int =
5 // Summarize conversation history every N messages

/*
NPC Behavior settings
Expand Down Expand Up @@ -196,6 +198,11 @@ class ConfigService(
"conversation.delayedPlayerMessageProcessing",
false,
) // Whether to delay player message processing like /g command
summarizationThreshold =
config.getInt(
"conversation.summarizationThreshold",
5,
) // Summarize conversation history every N messages

// NPC Behavior Settings
headRotationDelay = config.getInt("npc.headRotationDelay", 2)
Expand Down Expand Up @@ -290,6 +297,7 @@ class ConfigService(
config.set("conversation.behavioralDirectivesEnabled", behavioralDirectivesEnabled)
config.set("conversation.dialoguePathSelectionEnabled", dialoguePathSelectionEnabled)
config.set("conversation.delayedPlayerMessageProcessing", delayedPlayerMessageProcessing)
config.set("conversation.summarizationThreshold", summarizationThreshold)

// NPC Behavior settings
config.set("npc.headRotationDelay", headRotationDelay)
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/com/canefe/story/config/PromptService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ class PromptService(
return prompt
}

/** Gets the theme analysis prompt */
fun getThemeAnalysisPrompt(
availableThemes: String,
activeThemes: String,
): String {
val variables =
mapOf(
"available_themes" to availableThemes,
"active_themes" to activeThemes,
)
return getPrompt("theme_analysis", variables)
}

/** Gets the behavioral directive prompt with context */
fun getBehavioralDirectivePrompt(
recentMessages: String,
Expand Down Expand Up @@ -248,6 +261,9 @@ class PromptService(
return getPrompt("location_context_generation", variables)
}

/** Gets the message history summary prompt for mid-conversation summarization */
fun getMessageHistorySummaryPrompt(): String = getPrompt("message_history_summary")

/** Gets the session history summary prompt */
fun getSessionHistorySummaryPrompt(): String = getPrompt("session_history_summary")

Expand Down
34 changes: 34 additions & 0 deletions src/main/kotlin/com/canefe/story/conversation/Conversation.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.canefe.story.conversation

import com.canefe.story.conversation.theme.ConversationThemeData
import com.canefe.story.util.EssentialsUtils
import net.citizensnpcs.api.npc.NPC
import org.bukkit.entity.Player
Expand All @@ -14,6 +15,12 @@ class Conversation(
private val _npcs: MutableSet<NPC> = HashSet(initialNPCs)
private val _history: MutableList<ConversationMessage> = ArrayList()

// Track the number of non-system messages added since the last history summarization
var messagesSinceLastSummary: Int = 0

// Theme data for this conversation
val themeData: ConversationThemeData = ConversationThemeData()

// Public properties
var active: Boolean = true
var chatEnabled: Boolean = true
Expand Down Expand Up @@ -157,6 +164,9 @@ class Conversation(
message,
)
_history.add(userMessage)
if (message != "...") {
messagesSinceLastSummary++
}
}

private fun addAssistantMessage(message: String) {
Expand All @@ -166,6 +176,30 @@ class Conversation(
message,
)
_history.add(assistantMessage)
messagesSinceLastSummary++
}

fun replaceHistoryWithSummary(
summary: String,
summarizedMessagesCount: Int,
countedMessages: Int = summarizedMessagesCount,
) {
if (summarizedMessagesCount <= 0 || _history.size < summarizedMessagesCount) {
return
}
// Remove only the messages that were actually summarized
_history.subList(0, summarizedMessagesCount).clear()
// Prepend the new summary
_history.add(0, ConversationMessage("system", "Summary of conversation so far: $summary"))

// Decrement only by the number of messages that were actually counted
// toward the summarization threshold (excludes system messages and "..."
// placeholders). Messages added during the async window will have
// incremented the counter and must be preserved.
messagesSinceLastSummary -= countedMessages
if (messagesSinceLastSummary < 0) {
messagesSinceLastSummary = 0
}
}

fun clearHistory() {
Expand Down
Loading
Loading