feat(context,infra): compression pipeline stages 4-5 — token pruning + relevance + ToMe

- build() is now suspend: pipeline runs all stages in fixed order, each gated by level
- TOKEN_PRUNE (level 3): TokenPruner interface + LLMLingua-2 sidecar (sidecars/llmlingua)
  + HttpTokenPruner adapter (fails open if sidecar down); prunes freeform, preserves
  protected spans, skips tier-0 turns when TIER_SPLIT on
- TOME_MERGE (level 8): ToMeMerger collapses near-duplicate freeform turns (Jaccard)
- Stage 5 selection: RelevanceScorer + EmbeddingRelevanceScorer (cosine over Embedder);
  query-conditioned reorder so least-relevant freeform drops first under budget
- [orchestration] compression_level + token_pruner_url config, wired in Main
- suspend ripple fixed across builder callers/stubs
This commit is contained in:
2026-07-01 14:29:56 +04:00
parent e0c222392c
commit 047e2a4070
25 changed files with 509 additions and 65 deletions
+24
View File
@@ -0,0 +1,24 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
ext {
ktor_version = '3.0.3'
}
dependencies {
implementation project(':core:context')
implementation "org.slf4j:slf4j-api"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-cio:$ktor_version"
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "io.ktor:ktor-client-mock:$ktor_version"
testImplementation "org.jetbrains.kotlin:kotlin-test"
}
tasks.named("koverVerify").configure { enabled = false }
@@ -0,0 +1,58 @@
package com.correx.infrastructure.compression
import com.correx.core.context.compression.TokenPruner
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.engine.cio.CIO
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
/**
* [TokenPruner] backed by the LLMLingua-2 sidecar (sidecars/llmlingua/server.py) over localhost
* HTTP. Protected spans are forwarded so the classifier keeps them verbatim. Fails *open*: any
* error (sidecar down, timeout, malformed) returns the original content — compression is an
* optimization, never a correctness dependency, so a missing sidecar must not break inference.
*/
class HttpTokenPruner(
private val baseUrl: String = "http://127.0.0.1:8199",
private val timeoutMs: Long = DEFAULT_TIMEOUT_MS,
private val client: HttpClient = defaultClient(),
) : TokenPruner {
private val log = LoggerFactory.getLogger(HttpTokenPruner::class.java)
override suspend fun prune(content: String, protectedSpans: List<String>, targetRatio: Double): String =
runCatching {
withTimeout(timeoutMs) {
val resp: PruneResponse = client.post("$baseUrl/prune") {
contentType(ContentType.Application.Json)
setBody(PruneRequest(content, protectedSpans, targetRatio))
}.body()
resp.compressed
}
}.getOrElse { e ->
log.debug("token pruning sidecar unavailable ({}), passing content through", e.message)
content
}
@Serializable
private data class PruneRequest(val text: String, val protected: List<String>, val rate: Double)
@Serializable
private data class PruneResponse(val compressed: String)
private companion object {
const val DEFAULT_TIMEOUT_MS = 5000L
fun defaultClient() = HttpClient(CIO) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
}
}
@@ -0,0 +1,41 @@
import com.correx.infrastructure.compression.HttpTokenPruner
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
class HttpTokenPrunerTest {
private fun client(engine: MockEngine) = HttpClient(engine) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}
@Test
fun `returns sidecar compressed output`() = runBlocking {
val engine = MockEngine {
respond(
content = """{"compressed":"short text 10.0.0.1"}""",
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json"),
)
}
val pruner = HttpTokenPruner(client = client(engine))
assertEquals("short text 10.0.0.1", pruner.prune("a very long text 10.0.0.1", listOf("10.0.0.1"), 0.5))
}
@Test
fun `fails open when sidecar errors`() = runBlocking {
val engine = MockEngine { respond("boom", HttpStatusCode.InternalServerError) }
val pruner = HttpTokenPruner(client = client(engine))
val original = "keep this content untouched"
assertEquals(original, pruner.prune(original, emptyList(), 0.5))
}
}