feat(config): expose router generation + narration knobs in config file
The router/narration model behaviour was hardcoded (Main built RouterConfig() with defaults). Surface it under the [router] section so it's tunable without a rebuild: [router] conversation_keep_last, retrieval_k, token_budget [router.generation] temperature, top_p, max_tokens (chat/steering) [router.narration] temperature, top_p, max_tokens, max_per_run ConfigLoader parses the new sections (adds asDouble); Main maps the config-layer RouterConfig onto the domain RouterConfig and threads narration.max_per_run into ServerModule. All values default to the previous constants, so behaviour is unchanged when the sections are absent. Documents the block in sample-config.toml and adds parser tests for present/absent cases.
This commit is contained in:
@@ -14,10 +14,12 @@ import com.correx.core.config.CorrexConfig
|
|||||||
import com.correx.core.config.ProviderConfig
|
import com.correx.core.config.ProviderConfig
|
||||||
import com.correx.core.context.builder.DefaultContextPackBuilder
|
import com.correx.core.context.builder.DefaultContextPackBuilder
|
||||||
import com.correx.core.context.compression.DefaultContextCompressor
|
import com.correx.core.context.compression.DefaultContextCompressor
|
||||||
|
import com.correx.core.context.model.TokenBudget
|
||||||
import com.correx.core.events.EventDispatcher
|
import com.correx.core.events.EventDispatcher
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.inference.CapabilityScore
|
import com.correx.core.inference.CapabilityScore
|
||||||
import com.correx.core.inference.DefaultInferenceRouter
|
import com.correx.core.inference.DefaultInferenceRouter
|
||||||
|
import com.correx.core.inference.GenerationConfig
|
||||||
import com.correx.core.inference.InferenceProjector
|
import com.correx.core.inference.InferenceProjector
|
||||||
import com.correx.core.inference.InferenceRepository
|
import com.correx.core.inference.InferenceRepository
|
||||||
import com.correx.core.inference.InferenceRouter
|
import com.correx.core.inference.InferenceRouter
|
||||||
@@ -246,10 +248,26 @@ fun main() {
|
|||||||
// persisted vectors survive restart but the metadata map does not (no-op for
|
// persisted vectors survive restart but the metadata map does not (no-op for
|
||||||
// non-rehydratable backends like in_memory).
|
// non-rehydratable backends like in_memory).
|
||||||
runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) }
|
runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) }
|
||||||
|
// Map the config-file [router] block onto the domain RouterConfig the facade uses.
|
||||||
|
val domainRouterConfig = RouterConfig(
|
||||||
|
conversationKeepLast = routerConfig.conversationKeepLast,
|
||||||
|
retrievalK = routerConfig.retrievalK,
|
||||||
|
tokenBudget = TokenBudget(limit = routerConfig.tokenBudget),
|
||||||
|
generationConfig = GenerationConfig(
|
||||||
|
temperature = routerConfig.generation.temperature,
|
||||||
|
topP = routerConfig.generation.topP,
|
||||||
|
maxTokens = routerConfig.generation.maxTokens,
|
||||||
|
),
|
||||||
|
narrationGenerationConfig = GenerationConfig(
|
||||||
|
temperature = routerConfig.narration.temperature,
|
||||||
|
topP = routerConfig.narration.topP,
|
||||||
|
maxTokens = routerConfig.narration.maxTokens,
|
||||||
|
),
|
||||||
|
)
|
||||||
val routerFacade = InfrastructureModule.createRouterFacade(
|
val routerFacade = InfrastructureModule.createRouterFacade(
|
||||||
eventStore = eventStore,
|
eventStore = eventStore,
|
||||||
inferenceRouter = inferenceRouter,
|
inferenceRouter = inferenceRouter,
|
||||||
config = RouterConfig(),
|
config = domainRouterConfig,
|
||||||
tokenizer = firstProvider.tokenizer,
|
tokenizer = firstProvider.tokenizer,
|
||||||
embedder = embedder,
|
embedder = embedder,
|
||||||
l3MemoryStore = l3MemoryStore,
|
l3MemoryStore = l3MemoryStore,
|
||||||
@@ -289,6 +307,7 @@ fun main() {
|
|||||||
modelSwapper = modelSwapper,
|
modelSwapper = modelSwapper,
|
||||||
resourceProbe = resourceProbe,
|
resourceProbe = resourceProbe,
|
||||||
workspaceResolver = workspaceResolver,
|
workspaceResolver = workspaceResolver,
|
||||||
|
narrationMaxPerRun = routerConfig.narration.maxPerRun,
|
||||||
)
|
)
|
||||||
module.start()
|
module.start()
|
||||||
log.info("==============================")
|
log.info("==============================")
|
||||||
|
|||||||
@@ -233,8 +233,11 @@ object ConfigLoader {
|
|||||||
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
|
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
|
||||||
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
|
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
|
||||||
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
||||||
|
val routerSection = sections["router"] ?: emptyMap()
|
||||||
val routerEmbedderSection = sections["router.embedder"] ?: emptyMap()
|
val routerEmbedderSection = sections["router.embedder"] ?: emptyMap()
|
||||||
val routerL3Section = sections["router.l3"] ?: emptyMap()
|
val routerL3Section = sections["router.l3"] ?: emptyMap()
|
||||||
|
val routerGenerationSection = sections["router.generation"] ?: emptyMap()
|
||||||
|
val routerNarrationSection = sections["router.narration"] ?: emptyMap()
|
||||||
val modelsSection = sections["models"] ?: emptyMap()
|
val modelsSection = sections["models"] ?: emptyMap()
|
||||||
|
|
||||||
val server = ServerConfig(
|
val server = ServerConfig(
|
||||||
@@ -368,9 +371,27 @@ object ConfigLoader {
|
|||||||
bitWidth = asInt(routerL3Section["bit_width"], 4),
|
bitWidth = asInt(routerL3Section["bit_width"], 4),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val generation = GenerationSettings(
|
||||||
|
temperature = asDouble(routerGenerationSection["temperature"], 0.7),
|
||||||
|
topP = asDouble(routerGenerationSection["top_p"], 0.9),
|
||||||
|
maxTokens = asInt(routerGenerationSection["max_tokens"], 512),
|
||||||
|
)
|
||||||
|
|
||||||
|
val narration = NarrationSettings(
|
||||||
|
temperature = asDouble(routerNarrationSection["temperature"], 0.7),
|
||||||
|
topP = asDouble(routerNarrationSection["top_p"], 0.9),
|
||||||
|
maxTokens = asInt(routerNarrationSection["max_tokens"], 1024),
|
||||||
|
maxPerRun = asInt(routerNarrationSection["max_per_run"], 100),
|
||||||
|
)
|
||||||
|
|
||||||
val router = RouterConfig(
|
val router = RouterConfig(
|
||||||
embedder = embedder,
|
embedder = embedder,
|
||||||
l3 = l3,
|
l3 = l3,
|
||||||
|
conversationKeepLast = asInt(routerSection["conversation_keep_last"], 6),
|
||||||
|
retrievalK = asInt(routerSection["retrieval_k"], 5),
|
||||||
|
tokenBudget = asInt(routerSection["token_budget"], 4096),
|
||||||
|
generation = generation,
|
||||||
|
narration = narration,
|
||||||
)
|
)
|
||||||
|
|
||||||
val models = modelsList.mapNotNull { modelMap ->
|
val models = modelsList.mapNotNull { modelMap ->
|
||||||
@@ -457,6 +478,16 @@ object ConfigLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun asDouble(value: Any?, default: Double = 0.0): Double {
|
||||||
|
return when (value) {
|
||||||
|
is Double -> value
|
||||||
|
is Int -> value.toDouble()
|
||||||
|
is Long -> value.toDouble()
|
||||||
|
is String -> value.toDoubleOrNull() ?: default
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun asBoolean(value: Any?, default: Boolean = false): Boolean {
|
private fun asBoolean(value: Any?, default: Boolean = false): Boolean {
|
||||||
return when (value) {
|
return when (value) {
|
||||||
is Boolean -> value
|
is Boolean -> value
|
||||||
|
|||||||
@@ -89,6 +89,29 @@ data class ProviderConfig(
|
|||||||
data class RouterConfig(
|
data class RouterConfig(
|
||||||
val embedder: EmbedderConfig = EmbedderConfig(),
|
val embedder: EmbedderConfig = EmbedderConfig(),
|
||||||
val l3: L3Config = L3Config(),
|
val l3: L3Config = L3Config(),
|
||||||
|
val conversationKeepLast: Int = 6,
|
||||||
|
val retrievalK: Int = 5,
|
||||||
|
val tokenBudget: Int = 4096,
|
||||||
|
val generation: GenerationSettings = GenerationSettings(),
|
||||||
|
val narration: NarrationSettings = NarrationSettings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Sampling/length knobs for router chat + steering replies. */
|
||||||
|
@Serializable
|
||||||
|
data class GenerationSettings(
|
||||||
|
val temperature: Double = 0.7,
|
||||||
|
val topP: Double = 0.9,
|
||||||
|
val maxTokens: Int = 512,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Router narration knobs. Larger maxTokens than chat so reasoning models can
|
||||||
|
* finish "thinking" and still emit the line; maxPerRun caps narrations per run. */
|
||||||
|
@Serializable
|
||||||
|
data class NarrationSettings(
|
||||||
|
val temperature: Double = 0.7,
|
||||||
|
val topP: Double = 0.9,
|
||||||
|
val maxTokens: Int = 1024,
|
||||||
|
val maxPerRun: Int = 100,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -415,4 +415,51 @@ class ConfigLoaderTest {
|
|||||||
assertEquals("/home/user/projects", result.tools.allowedWorkspaceRoots[0])
|
assertEquals("/home/user/projects", result.tools.allowedWorkspaceRoots[0])
|
||||||
assertEquals("/tmp/work", result.tools.allowedWorkspaceRoots[1])
|
assertEquals("/tmp/work", result.tools.allowedWorkspaceRoots[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses router generation and narration knobs`() {
|
||||||
|
val toml = """
|
||||||
|
[router]
|
||||||
|
conversation_keep_last = 8
|
||||||
|
retrieval_k = 3
|
||||||
|
token_budget = 8192
|
||||||
|
|
||||||
|
[router.generation]
|
||||||
|
temperature = 0.4
|
||||||
|
top_p = 0.8
|
||||||
|
max_tokens = 600
|
||||||
|
|
||||||
|
[router.narration]
|
||||||
|
temperature = 0.55
|
||||||
|
top_p = 0.95
|
||||||
|
max_tokens = 2048
|
||||||
|
max_per_run = 25
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val parseTomlMethod = ConfigLoader::class.java.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(8, result.router.conversationKeepLast)
|
||||||
|
assertEquals(3, result.router.retrievalK)
|
||||||
|
assertEquals(8192, result.router.tokenBudget)
|
||||||
|
assertEquals(0.4, result.router.generation.temperature)
|
||||||
|
assertEquals(0.8, result.router.generation.topP)
|
||||||
|
assertEquals(600, result.router.generation.maxTokens)
|
||||||
|
assertEquals(0.55, result.router.narration.temperature)
|
||||||
|
assertEquals(2048, result.router.narration.maxTokens)
|
||||||
|
assertEquals(25, result.router.narration.maxPerRun)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml uses router defaults when sections absent`() {
|
||||||
|
val parseTomlMethod = ConfigLoader::class.java.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, "[server]\nhost = \"x\"") as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(6, result.router.conversationKeepLast)
|
||||||
|
assertEquals(512, result.router.generation.maxTokens)
|
||||||
|
assertEquals(1024, result.router.narration.maxTokens)
|
||||||
|
assertEquals(100, result.router.narration.maxPerRun)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,26 @@ capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0
|
|||||||
# capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0.8, ToolCalling = 0.5 }
|
# capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0.8, ToolCalling = 0.5 }
|
||||||
|
|
||||||
# Router configuration (optional, defaults shown below)
|
# Router configuration (optional, defaults shown below)
|
||||||
|
[router]
|
||||||
|
conversation_keep_last = 6 # how many recent chat turns to keep in context
|
||||||
|
retrieval_k = 5 # L3 memory hits to retrieve per query
|
||||||
|
token_budget = 4096 # context budget for router prompts
|
||||||
|
|
||||||
|
# Sampling/length for router chat + steering replies.
|
||||||
|
[router.generation]
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
max_tokens = 512
|
||||||
|
|
||||||
|
# Router narration (the live workflow commentary). max_tokens is higher than chat so
|
||||||
|
# reasoning models can finish "thinking" and still emit the line; max_per_run caps how
|
||||||
|
# many narrations fire per workflow run.
|
||||||
|
[router.narration]
|
||||||
|
temperature = 0.7
|
||||||
|
top_p = 0.9
|
||||||
|
max_tokens = 1024
|
||||||
|
max_per_run = 100
|
||||||
|
|
||||||
[router.embedder]
|
[router.embedder]
|
||||||
backend = "noop" # or "llamacpp"
|
backend = "noop" # or "llamacpp"
|
||||||
dimension = 1536
|
dimension = 1536
|
||||||
|
|||||||
Reference in New Issue
Block a user