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
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:
commit
3e018f528a
15 changed files with 1781 additions and 0 deletions
124
src/lib.rs
Normal file
124
src/lib.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue