text.anonymize.legal-de 0.1.0: WASM bridge to the judge-ner host service
Some checks failed
CI / Linux x86_64 (Forgejo) (push) Failing after 1m20s

NER anonymization of German legal texts (plain text + DOCX) via the
JuraNER model by Harshil Darji (MIT). The transformer model runs in
the judge-ner host service; this module forwards documents over
loopback HTTP and maps responses onto the capability contract.
This commit is contained in:
flemming-it 2026-07-10 12:44:32 +02:00
commit 3e018f528a
15 changed files with 1781 additions and 0 deletions

124
src/lib.rs Normal file
View file

@ -0,0 +1,124 @@
//! `text.anonymize.legal-de` — NER anonymization of German legal
//! texts, bridged to the `judge-ner` host service.
//!
//! The heavy lifting (JuraNER transformers model, DOCX rewriting)
//! happens in the judge-ner container — see
//! `fai_judge/host_services/judge-ner/`. This module forwards the
//! document over loopback HTTP and maps the response onto the frozen
//! capability contract:
//!
//! inputs: `request` (json) {"mode":"text"|"docx","options":{...}}
//! `document` (bytes) UTF-8 text or DOCX
//! `endpoint` (text, optional) service base URL override
//! outputs: `result` (json) {"replacements":[...], "text" only for
//! mode=text}
//! `document` (bytes) anonymized text/DOCX
mod anonymize;
pub use anonymize::DEFAULT_ENDPOINT;
use chain_module_sdk::prelude::*;
use crate::anonymize::{AnonError, HttpClient, HttpResponse};
#[fai_module]
pub fn invoke(_ctx: Context, inputs: Inputs) -> Result<Outputs, ModuleError> {
let request_raw = read_json_or_text(&inputs, "request")?;
let request = anonymize::parse_request(&request_raw).map_err(to_module_error)?;
let document = inputs.require_bytes("document")?;
let endpoint = inputs
.get("endpoint")
.and_then(payload_text)
.unwrap_or_default();
let client = make_client();
let outcome = anonymize::run(&client, &request, &document.data, &endpoint)
.map_err(to_module_error)?;
Ok(Outputs::new()
.with_json_str("result", outcome.result_json)
.with_bytes("document", outcome.document_mime, outcome.document))
}
fn to_module_error(err: AnonError) -> ModuleError {
match err {
AnonError::InvalidInput(msg) => ModuleError::invalid_input(msg),
AnonError::Service(msg) => ModuleError::internal(msg),
}
}
fn payload_text(p: &Payload) -> Option<String> {
match p {
Payload::Text(s) => Some(s.clone()),
_ => None,
}
}
/// Accept the request payload either as `json` (the documented form)
/// or `text` (the form gRPC clients without first-class JSON-payload
/// support fall through to). Both carry the same JSON string.
fn read_json_or_text(inputs: &Inputs, name: &str) -> Result<String, ModuleError> {
if let Ok(s) = inputs.require_json_str(name) {
return Ok(s.to_string());
}
if let Ok(s) = inputs.require_text(name) {
return Ok(s.to_string());
}
Err(ModuleError::invalid_input(format!(
"input '{name}' must be json or text (carrying a JSON string)"
)))
}
#[cfg(target_arch = "wasm32")]
fn make_client() -> WakiClient {
WakiClient
}
#[cfg(not(target_arch = "wasm32"))]
fn make_client() -> HostStubClient {
HostStubClient
}
/// Host builds (unit tests, `cargo test`) have no outbound HTTP —
/// only the wasm32 build talks to the service.
#[cfg(not(target_arch = "wasm32"))]
struct HostStubClient;
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
impl HttpClient for HostStubClient {
fn post_json(&self, _url: &str, _body: &str) -> Result<HttpResponse, AnonError> {
Err(AnonError::Service(
"HTTP path is unavailable on the host build; only wasm32 supports outbound HTTP"
.to_string(),
))
}
}
#[cfg(target_arch = "wasm32")]
struct WakiClient;
#[cfg(target_arch = "wasm32")]
impl HttpClient for WakiClient {
fn post_json(&self, url: &str, body: &str) -> Result<HttpResponse, AnonError> {
let response = waki::Client::new()
.post(url)
.header("Content-Type", "application/json")
.body(body.to_string())
.send()
.map_err(|e| {
AnonError::Service(format!(
"judge-ner service unreachable at {url}: {e} \
(is the judge-ner container running?)"
))
})?;
let status = response.status_code();
let bytes = response
.body()
.map_err(|e| AnonError::Service(format!("read response body: {e}")))?;
let body = String::from_utf8(bytes)
.map_err(|e| AnonError::Service(format!("response body is not UTF-8: {e}")))?;
Ok(HttpResponse { status, body })
}
}