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:
@@ -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 }
|
||||
+58
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user