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.
124 lines
4.1 KiB
Rust
124 lines
4.1 KiB
Rust
//! `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 })
|
|
}
|
|
}
|