diff --git a/Cargo.lock b/Cargo.lock index 507b6e42..96c21fad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2448,6 +2448,7 @@ version = "0.1.0" dependencies = [ "derive_builder", "derive_more", + "elide-core", "elide-llm", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2eebb316..0a1b88d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,18 +32,17 @@ documentation = "https://docs.rs/nvisy-runtime" # # See for more details: https://github.com/rust-lang/cargo/issues/11329 -# Elide toolkit (upstream) +# Elide crates elide = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } elide-core = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } elide-detection = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } elide-redaction = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } -# Pulled directly only by `elide-bento`, which implements the per-backend traits these crates export. elide-llm = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } elide-ner = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } elide-ocr = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } elide-stt = { git = "https://github.com/nvisycom/elide", branch = "main", default-features = false } -# Runtime-owned elide extensions +# Elide extensions elide-bento = { path = "./crates/elide-bento", version = "0.1.0" } # Internal crates diff --git a/crates/elide-bento/Cargo.toml b/crates/elide-bento/Cargo.toml index f7b5d2cb..310df439 100644 --- a/crates/elide-bento/Cargo.toml +++ b/crates/elide-bento/Cargo.toml @@ -32,7 +32,7 @@ ner = ["dep:elide-ner"] ocr = ["dep:elide-ocr", "dep:base64", "elide-core/image"] [dependencies] -# Elide toolkit (upstream) +# Elide crates elide-core = { workspace = true, features = [] } elide-ner = { workspace = true, features = [], optional = true } elide-ocr = { workspace = true, features = [], optional = true } @@ -40,20 +40,21 @@ elide-ocr = { workspace = true, features = [], optional = true } # Serialization serde = { workspace = true, features = ["derive"] } +# Derive macros and error handling +thiserror = { workspace = true, features = [] } + # Primitive datatypes hipstr = { workspace = true, features = [] } -# Async runtime -async-trait = { workspace = true, features = [] } - -# Error handling -thiserror = { workspace = true, features = [] } - -# Image bytes → base64 (OCR only) +# Encoding and hashing base64 = { workspace = true, features = [], optional = true } -# BentoML client +# Async runtime and parallelism +async-trait = { workspace = true, features = [] } + +# AI / LLM frameworks bentoml = { workspace = true, default-features = false, features = ["rustls-tls", "tracing"] } [dev-dependencies] +# Async runtime and parallelism tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/nvisy-context/Cargo.toml b/crates/nvisy-context/Cargo.toml index e2671526..26fc0c25 100644 --- a/crates/nvisy-context/Cargo.toml +++ b/crates/nvisy-context/Cargo.toml @@ -23,16 +23,14 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -# Elide's stable wire vocabulary: primitive types (LanguageTag, -# BoundingBox, TimeSpan) plus modality types. No codec, no LLM, no -# runtime plumbing — elide-core is the SDK-safe subset. +# Elide crates elide-core = { workspace = true, features = ["serde", "schema"] } # Serialization serde = { workspace = true, features = ["derive"] } schemars = { workspace = true, features = ["uuid1", "semver1"] } -# Derive macros +# Derive macros and error handling derive_builder = { workspace = true, features = [] } derive_more = { workspace = true, features = ["from"] } @@ -42,5 +40,5 @@ jiff = { workspace = true, features = ["serde"] } semver = { workspace = true, features = ["serde"] } [dev-dependencies] -# Round-trip tests serialize + deserialize schema values. +# Serialization serde_json = { workspace = true, features = [] } diff --git a/crates/nvisy-core/Cargo.toml b/crates/nvisy-core/Cargo.toml index b39086db..534bcd01 100644 --- a/crates/nvisy-core/Cargo.toml +++ b/crates/nvisy-core/Cargo.toml @@ -31,21 +31,17 @@ default = [] test-utils = [] [dependencies] -# LLM types (`Provider`, `AuthenticatedProvider`, `UnauthenticatedProvider`). -# The provider features gate their enum variants — turning them on is what -# makes `Provider::OpenAi` etc. exist. Rig comes along for the ride. +# Elide crates +elide-core = { workspace = true, features = [] } elide-llm = { workspace = true, features = ["rig", "openai-gpt", "anthropic-claude", "google-gemini"] } -# Serialization — needed for the LLM config's derive-serde types. +# Serialization serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = [] } schemars = { workspace = true, features = [] } -# Error type deps: `Error::From` + -# `Error::From` map upstream -# errors into the shared vocabulary. +# Derive macros and error handling thiserror = { workspace = true, features = [] } -strum = { workspace = true, features = ["derive"] } derive_more = { workspace = true, features = ["display"] } derive_builder = { workspace = true, features = [] } - +strum = { workspace = true, features = ["derive"] } diff --git a/crates/nvisy-core/src/error.rs b/crates/nvisy-core/src/error.rs index a3b4dee6..ae142e16 100644 --- a/crates/nvisy-core/src/error.rs +++ b/crates/nvisy-core/src/error.rs @@ -273,5 +273,26 @@ impl From for Error { } } +impl From for Error { + /// Map elide's per-operation error into the runtime's shared + /// vocabulary. The elide `ErrorKind` is preserved semantically + /// via a mapping onto the nearest [`ErrorKind`] variant; the + /// original elide error travels along as the source cause. + /// + /// Called at every `nvisy-engine` seam where an `elide::Error` + /// crosses into engine-land — pattern compile, recognizer + /// build, anonymizer attach, orchestrator analyze/anonymize. + fn from(err: elide_core::Error) -> Self { + let kind = match err.kind() { + elide_core::ErrorKind::OutOfRange | elide_core::ErrorKind::Validation => { + ErrorKind::Validation + } + elide_core::ErrorKind::Transport => ErrorKind::Connection, + _ => ErrorKind::Runtime, + }; + Self::new(kind, err.to_string()).with_source(err) + } +} + /// Convenience type alias for results using the Nvisy error type. pub type Result = result::Result; diff --git a/crates/nvisy-engine/Cargo.toml b/crates/nvisy-engine/Cargo.toml index 8daeff48..44f0b4bb 100644 --- a/crates/nvisy-engine/Cargo.toml +++ b/crates/nvisy-engine/Cargo.toml @@ -74,56 +74,45 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -# Internal crates -nvisy-core = { workspace = true, features = [] } -nvisy-schema = { workspace = true, features = [] } - -# Elide toolkit (upstream). `mock` pulls the no-op NER stub; the LLM -# features pull the rig backend (`llm-rig`, needed for Ollama) plus -# the three authenticated providers. +# Elide crates elide = { workspace = true, features = ["codec", "codec-text", "pattern", "ner", "llm", "llm-rig", "llm-openai", "llm-anthropic", "llm-gemini", "lingua", "serde", "schema", "sha2"] } - -# `Jinja2Prompt` loader for deployment-configured per-recognizer -# prompt templates. Not re-exported from the elide umbrella, so -# we depend directly. -elide-llm = { workspace = true, features = ["jinja2"] } elide-core = { workspace = true, features = ["serde", "schema", "image", "audio", "tabular"] } - -# Recognition-side elide crates not re-exported through the elide -# umbrella the way the recognizer crates are (`elide::recognition::*` -# is the natural reach for those). `elide-ocr` is the OCR enricher -# layer engine wires from the image-modality compile path. +elide-llm = { workspace = true, features = ["jinja2"] } elide-ocr = { workspace = true, features = [] } elide-stt = { workspace = true, features = [] } -# Runtime-owned elide extensions +# Elide extensions elide-bento = { workspace = true, features = ["ner", "ocr"] } +# Internal crates +nvisy-core = { workspace = true, features = [] } +nvisy-schema = { workspace = true, features = [] } + +# Serialization +serde = { workspace = true, features = ["derive"] } +schemars = { workspace = true, features = [] } + # Primitive datatypes uuid = { workspace = true, features = [] } bytes = { workspace = true, features = [] } -serde = { workspace = true, features = ["derive"] } -schemars = { workspace = true, features = [] } -# Structured logging — surface non-fatal anomalies (unknown -# builtin label name in a catalog request, etc.). +# Observability tracing = { workspace = true, features = [] } [dev-dependencies] -# Turn on the test-only mock variants -# (`{Ner,Ocr,Stt,Llm}BackendConfig::Mock`) so integration tests -# can construct a working analyzer without hitting a real Bento -# backend or LLM provider. +# Internal crates nvisy-engine = { path = ".", features = ["test-utils"] } -# Async runtime and parallelism -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +# Serialization +serde_json = { workspace = true, features = [] } -# Primitives referenced from test fixtures. -hipstr = { workspace = true, features = [] } +# Primitive datatypes uuid = { workspace = true, features = ["v7"] } bytes = { workspace = true, features = [] } +hipstr = { workspace = true, features = [] } # Test utilities zip = { workspace = true } -serde_json = { workspace = true, features = [] } + +# Async runtime and parallelism +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/nvisy-engine/src/analyzer/audio.rs b/crates/nvisy-engine/src/analyzer/audio.rs index 584812c9..c5bbb2ee 100644 --- a/crates/nvisy-engine/src/analyzer/audio.rs +++ b/crates/nvisy-engine/src/analyzer/audio.rs @@ -41,9 +41,7 @@ pub(super) fn compile( if let Some(pattern) = &spec.recognizers.pattern { analyzer = attach_pattern(analyzer, pattern, guardrails)?; } - if spec.recognizers.ner { - analyzer = attach_ner_lineup(analyzer, ner)?; - } + analyzer = attach_ner_lineup(analyzer, ner, spec.recognizers.ner)?; Ok(attach_dedup(analyzer, &spec.deduplication)) } diff --git a/crates/nvisy-engine/src/analyzer/image.rs b/crates/nvisy-engine/src/analyzer/image.rs index e5c1ead2..a8cbb17d 100644 --- a/crates/nvisy-engine/src/analyzer/image.rs +++ b/crates/nvisy-engine/src/analyzer/image.rs @@ -46,12 +46,13 @@ pub(super) fn compile( if let Some(pattern) = &spec.recognizers.pattern { analyzer = attach_pattern(analyzer, pattern, guardrails)?; } - if spec.recognizers.ner { - analyzer = attach_ner_lineup(analyzer, ner)?; - } - if spec.recognizers.llm { - analyzer = attach_llm_lineup(analyzer, llm, LlmRecognizerModality::Image)?; - } + analyzer = attach_ner_lineup(analyzer, ner, spec.recognizers.ner)?; + analyzer = attach_llm_lineup( + analyzer, + llm, + LlmRecognizerModality::Image, + spec.recognizers.llm, + )?; Ok(attach_dedup(analyzer, &spec.deduplication)) } diff --git a/crates/nvisy-engine/src/analyzer/recognizer/llm.rs b/crates/nvisy-engine/src/analyzer/recognizer/llm.rs index ddf001c5..985db497 100644 --- a/crates/nvisy-engine/src/analyzer/recognizer/llm.rs +++ b/crates/nvisy-engine/src/analyzer/recognizer/llm.rs @@ -18,14 +18,20 @@ use nvisy_core::llm::{ }; /// Attach every LLM recognizer in `llm.recognizers` whose -/// modality list includes `modality` to `analyzer`. Errors when: +/// modality list includes `modality` to `analyzer`, dispatched on +/// the request's three-state toggle. /// -/// - The lineup has no recognizer for this modality (compile is -/// only invoked when the request toggled `llm = true`, so -/// "nothing configured for this modality" is user-visible). -/// - A recognizer's `modalities` list is empty. -/// - A Jinja2 prompt file / template fails to load or compile. -/// - A provider-client construction (rig) fails. +/// - `Some(true)`: explicit opt-in. Attaches every configured +/// recognizer whose declared modalities include `modality`; +/// errors when zero match. +/// - `Some(false)`: explicit opt-out. Returns the analyzer +/// unchanged. +/// - `None`: softly-on default. Attaches every matching +/// recognizer if any match; skips silently otherwise. +/// +/// Errors on: any recognizer whose `modalities` list is empty +/// (bad config), Jinja2 prompt load/compile failure, provider +/// client construction failure. /// /// Bound explanation: /// @@ -34,10 +40,11 @@ use nvisy_core::llm::{ /// - `DefaultPrompt: Prompt` — elide ships text + image /// default prompts. /// - `Jinja2Prompt: Prompt` — same coverage. -pub(crate) fn attach_llm_lineup( +pub(in crate::analyzer) fn attach_llm_lineup( mut analyzer: Analyzer, llm: &LlmConfig, modality: LlmRecognizerModality, + toggle: Option, ) -> Result, Error> where M: LlmModality, @@ -45,6 +52,9 @@ where DefaultPrompt: Prompt, Jinja2Prompt: Prompt, { + if toggle == Some(false) { + return Ok(analyzer); + } let mut matched = 0usize; for recognizer in &llm.recognizers { if recognizer.modalities.is_empty() { @@ -63,13 +73,13 @@ where matched += 1; analyzer = attach_one(analyzer, recognizer)?; } - if matched == 0 { + if matched == 0 && toggle == Some(true) { return Err(Error::new( ErrorKind::Validation, format!( "AnalyzerParams.recognizers.llm = true but the deployment has no LLM \ recognizer configured for the {modality:?} modality; add one to \ - `[[llm.recognizers]]` in the deployment config or leave `llm = false`", + `[[llm.recognizers]]` in the deployment config or leave `llm` unset / false", ), )); } diff --git a/crates/nvisy-engine/src/analyzer/recognizer/ner.rs b/crates/nvisy-engine/src/analyzer/recognizer/ner.rs index b6df3da5..0e43dcb4 100644 --- a/crates/nvisy-engine/src/analyzer/recognizer/ner.rs +++ b/crates/nvisy-engine/src/analyzer/recognizer/ner.rs @@ -4,7 +4,8 @@ //! backend (or `MockBackend` under the `test-utils` feature). //! //! Symmetric with [`super::llm`]; called by every per-modality -//! compile function when `AnalyzerParams.recognizers.ner == true`. +//! compile function with the request's +//! `AnalyzerParams.recognizers.ner` three-state toggle. //! //! [`Analyzer`]: elide::detection::Analyzer //! [`NerConfig::recognizers`]: nvisy_core::ner::NerConfig::recognizers @@ -17,26 +18,37 @@ use elide_core::modality::TextRecognizable; use elide_core::recognition::Recognizer; use nvisy_core::ner::{NerBackendConfig, NerConfig, NerRecognizer as ConfigNerRecognizer}; -/// Attach every recognizer from the deployment's NER lineup. -/// Errors when the lineup is empty (compile is only invoked when -/// the request toggled `ner = true`, so "no recognizers -/// configured" is user-visible). Modality-generic for any -/// `M: TextRecognizable`. +/// Attach every recognizer from the deployment's NER lineup, +/// dispatched on the request's three-state toggle. +/// +/// - `Some(true)`: explicit opt-in. Attaches every configured +/// recognizer; errors if the lineup is empty. +/// - `Some(false)`: explicit opt-out. Returns the analyzer +/// unchanged. +/// - `None`: softly-on default. Attaches every configured +/// recognizer if the lineup is non-empty; skips silently +/// otherwise. pub(in crate::analyzer) fn attach_ner_lineup( mut analyzer: Analyzer, ner: &NerConfig, + toggle: Option, ) -> Result, Error> where M: TextRecognizable, NerRecognizer: Recognizer + 'static, { - if ner.recognizers.is_empty() { - return Err(Error::new( - elide_core::ErrorKind::Validation, - "AnalyzerParams.recognizers.ner = true but the deployment has no NER \ - recognizer configured; add one to `[[ner.recognizers]]` in the \ - deployment config or leave `ner = false`", - )); + match toggle { + Some(false) => return Ok(analyzer), + None if ner.recognizers.is_empty() => return Ok(analyzer), + Some(true) if ner.recognizers.is_empty() => { + return Err(Error::new( + elide_core::ErrorKind::Validation, + "AnalyzerParams.recognizers.ner = true but the deployment has no NER \ + recognizer configured; add one to `[[ner.recognizers]]` in the \ + deployment config or leave `ner` unset / false", + )); + } + _ => {} } for recognizer in &ner.recognizers { analyzer = attach_ner_one(analyzer, recognizer)?; diff --git a/crates/nvisy-engine/src/analyzer/tabular.rs b/crates/nvisy-engine/src/analyzer/tabular.rs index e74fc04e..fb257f84 100644 --- a/crates/nvisy-engine/src/analyzer/tabular.rs +++ b/crates/nvisy-engine/src/analyzer/tabular.rs @@ -35,9 +35,7 @@ pub(super) fn compile( if let Some(pattern) = &spec.recognizers.pattern { analyzer = attach_pattern(analyzer, pattern, guardrails)?; } - if spec.recognizers.ner { - analyzer = attach_ner_lineup(analyzer, ner)?; - } + analyzer = attach_ner_lineup(analyzer, ner, spec.recognizers.ner)?; Ok(attach_dedup(analyzer, &spec.deduplication)) } diff --git a/crates/nvisy-engine/src/analyzer/text.rs b/crates/nvisy-engine/src/analyzer/text.rs index 06aade23..c090f668 100644 --- a/crates/nvisy-engine/src/analyzer/text.rs +++ b/crates/nvisy-engine/src/analyzer/text.rs @@ -2,9 +2,11 @@ //! [`elide::detection::Analyzer`]. //! //! Text supports the full recognizer set: Pattern, NER, and LLM. -//! NER and LLM are opt-in via `spec.recognizers.ner = true` / -//! `spec.recognizers.llm = true`; the deployment's [`NerConfig`] -//! and [`LlmConfig`] provide the actual recognizer lineups. +//! NER and LLM are three-state toggles on +//! `spec.recognizers.{ner,llm}` (see +//! [`nvisy_schema::plan::RecognizerParams`]); the deployment's +//! [`NerConfig`] and [`LlmConfig`] provide the actual recognizer +//! lineups. //! //! Modality-foreign enrichers (`ocr`, `stt`) on `spec` are //! silently ignored; those flow through the modalities they @@ -44,12 +46,13 @@ pub(super) fn compile( if let Some(pattern) = &spec.recognizers.pattern { analyzer = attach_pattern(analyzer, pattern, guardrails)?; } - if spec.recognizers.ner { - analyzer = attach_ner_lineup(analyzer, ner)?; - } - if spec.recognizers.llm { - analyzer = attach_llm_lineup(analyzer, llm, LlmRecognizerModality::Text)?; - } + analyzer = attach_ner_lineup(analyzer, ner, spec.recognizers.ner)?; + analyzer = attach_llm_lineup( + analyzer, + llm, + LlmRecognizerModality::Text, + spec.recognizers.llm, + )?; Ok(attach_dedup(analyzer, &spec.deduplication)) } diff --git a/crates/nvisy-engine/src/anonymizer/compile/selector.rs b/crates/nvisy-engine/src/anonymizer/compile/selector.rs index 3367c1c6..6f1d9f10 100644 --- a/crates/nvisy-engine/src/anonymizer/compile/selector.rs +++ b/crates/nvisy-engine/src/anonymizer/compile/selector.rs @@ -52,7 +52,7 @@ pub(super) fn fallback_attribution(policy: &Policy) -> Attribution { /// sentinel `policy_id = "override"` so audits can distinguish /// override-driven redactions from policy-driven ones; `reason` /// carries the overridden entity's id. -pub(crate) fn override_attribution(entity_id: Uuid) -> Attribution { +pub(super) fn override_attribution(entity_id: Uuid) -> Attribution { Attribution { policy_id: "override".into(), reason: Some(entity_id.to_string().into()), @@ -63,7 +63,7 @@ pub(crate) fn override_attribution(entity_id: Uuid) -> Attribution { /// identified by `entity_id`. Used by the apply pipeline to give /// reviewer overrides higher precedence than any policy-driven /// rule. -pub(crate) fn attach_override( +pub(super) fn attach_override( anonymizer: Anonymizer, entity_id: Uuid, operator: O, diff --git a/crates/nvisy-engine/src/pipeline/mod.rs b/crates/nvisy-engine/src/pipeline/mod.rs index e5bc260d..d72295e5 100644 --- a/crates/nvisy-engine/src/pipeline/mod.rs +++ b/crates/nvisy-engine/src/pipeline/mod.rs @@ -99,9 +99,7 @@ use nvisy_schema::policy::{Policy, PolicyAction}; use uuid::Uuid; pub use self::analyzed::{AnalyzedDocument, EntityRecord, RecognizedGroup}; -use self::report::{ - collect_overrides_into, encode_redacted, insert_body, insert_part, take_body, take_part, -}; +use self::report::{take_body, take_part}; const COMPONENT: &str = "pipeline"; @@ -220,9 +218,7 @@ impl Engine { let extension = document.extension.clone(); let mut handle = self.decode(document).await?; let (orchestrator, scope) = self.build_analyze_orchestrator(spec, correlation_id)?; - let mut report = orchestrator.analyze(&mut handle).await.map_err(|err| { - Error::internal("orchestrator analyze failed", COMPONENT).with_source(err) - })?; + let mut report = orchestrator.analyze(&mut handle).await?; // Walk the body modality slots in order; the first that // returns Some is the body modality the orchestrator's @@ -312,12 +308,12 @@ impl Engine { })?; let correlation_id = document.correlation_id; let mut handle = self.decode(document).await?; - let mut report = insert_body(Report::new(), body_group); + let mut report = body_group.insert_into_body(Report::new()); let mut overrides: Vec<(Uuid, PolicyAction)> = Vec::new(); - collect_overrides_into(&mut overrides, body_group); + body_group.collect_overrides_into(&mut overrides); for (id, group) in &analyzed.parts { - report = insert_part(report, id.as_str(), group); - collect_overrides_into(&mut overrides, group); + report = group.insert_as_part(report, id.as_str()); + group.collect_overrides_into(&mut overrides); } let orchestrator = self.build_anonymize_orchestrator( @@ -326,14 +322,9 @@ impl Engine { &overrides, correlation_id, )?; - orchestrator - .anonymize_with(&mut handle, report) - .await - .map_err(|err| { - Error::internal("orchestrator anonymize_with failed", COMPONENT).with_source(err) - })?; + orchestrator.anonymize_with(&mut handle, report).await?; - encode_redacted(handle, body_group) + body_group.encode_redacted_from(handle) } async fn decode(&self, document: Document) -> Result { diff --git a/crates/nvisy-engine/src/pipeline/orchestrator.rs b/crates/nvisy-engine/src/pipeline/orchestrator.rs index bf0321f3..e4419c46 100644 --- a/crates/nvisy-engine/src/pipeline/orchestrator.rs +++ b/crates/nvisy-engine/src/pipeline/orchestrator.rs @@ -40,7 +40,7 @@ use elide_core::modality::image::Image; #[cfg(feature = "internal_tabular")] use elide_core::modality::tabular::Tabular; use elide_core::modality::text::Text; -use nvisy_core::{Error, Result}; +use nvisy_core::Result; use nvisy_schema::plan::AnalyzerParams; use nvisy_schema::policy::{Policy, PolicyAction}; use uuid::Uuid; @@ -55,8 +55,6 @@ use crate::anonymizer::{attach_override_image, attach_policies_image}; use crate::anonymizer::{attach_override_tabular, attach_policies_tabular}; use crate::anonymizer::{attach_override_text, attach_policies_text}; -const COMPONENT: &str = "pipeline::orchestrator"; - impl Engine { /// Build an [`Orchestrator`] for the analyze path: compile /// every per-modality analyzer from `spec`, wire empty @@ -89,41 +87,32 @@ impl Engine { }; let text_anon = assemble_empty::(&catalog); - let text_analyzer = spec - .compile_text(&self.ner, &self.llm, &self.pattern_guardrails) - .map_err(compile_err)?; + let text_analyzer = spec.compile_text(&self.ner, &self.llm, &self.pattern_guardrails)?; - #[allow(unused_mut)] - let mut orchestrator = Orchestrator::new(&self.formats) + let orchestrator = Orchestrator::new(&self.formats) .with_scope(live_scope) .with_modality::(text_analyzer, text_anon); #[cfg(feature = "internal_tabular")] - { + let orchestrator = { let anon = assemble_empty::(&catalog); - let analyzer = spec - .compile_tabular(&self.ner, &self.pattern_guardrails) - .map_err(compile_err)?; - orchestrator = orchestrator.with_modality::(analyzer, anon); - } + let analyzer = spec.compile_tabular(&self.ner, &self.pattern_guardrails)?; + orchestrator.with_modality::(analyzer, anon) + }; #[cfg(feature = "internal_image")] - { + let orchestrator = { let anon = assemble_empty::(&catalog); - let analyzer = spec - .compile_image(&self.ner, &self.llm, &self.pattern_guardrails) - .map_err(compile_err)?; - orchestrator = orchestrator.with_modality::(analyzer, anon); - } + let analyzer = spec.compile_image(&self.ner, &self.llm, &self.pattern_guardrails)?; + orchestrator.with_modality::(analyzer, anon) + }; #[cfg(feature = "internal_audio")] - { + let orchestrator = { let anon = assemble_empty::