Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 10 additions & 9 deletions crates/elide-bento/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,29 @@ 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 }

# 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"] }
8 changes: 3 additions & 5 deletions crates/nvisy-context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand All @@ -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 = [] }
14 changes: 5 additions & 9 deletions crates/nvisy-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Error>` +
# `Error::From<derive_builder::UninitializedFieldError>` 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"] }
21 changes: 21 additions & 0 deletions crates/nvisy-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,5 +273,26 @@ impl From<derive_builder::UninitializedFieldError> for Error {
}
}

impl From<elide_core::Error> 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<T, E = Error> = result::Result<T, E>;
51 changes: 20 additions & 31 deletions crates/nvisy-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
4 changes: 1 addition & 3 deletions crates/nvisy-engine/src/analyzer/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
13 changes: 7 additions & 6 deletions crates/nvisy-engine/src/analyzer/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
30 changes: 20 additions & 10 deletions crates/nvisy-engine/src/analyzer/recognizer/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
///
Expand All @@ -34,17 +40,21 @@ use nvisy_core::llm::{
/// - `DefaultPrompt: Prompt<M>` — elide ships text + image
/// default prompts.
/// - `Jinja2Prompt<M>: Prompt<M>` — same coverage.
pub(crate) fn attach_llm_lineup<M>(
pub(in crate::analyzer) fn attach_llm_lineup<M>(
mut analyzer: Analyzer<M>,
llm: &LlmConfig,
modality: LlmRecognizerModality,
toggle: Option<bool>,
) -> Result<Analyzer<M>, Error>
where
M: LlmModality,
RigBackend: LlmBackend<M>,
DefaultPrompt: Prompt<M>,
Jinja2Prompt<M>: Prompt<M>,
{
if toggle == Some(false) {
return Ok(analyzer);
}
let mut matched = 0usize;
for recognizer in &llm.recognizers {
if recognizer.modalities.is_empty() {
Expand All @@ -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",
),
));
}
Expand Down
38 changes: 25 additions & 13 deletions crates/nvisy-engine/src/analyzer/recognizer/ner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<M>(
mut analyzer: Analyzer<M>,
ner: &NerConfig,
toggle: Option<bool>,
) -> Result<Analyzer<M>, Error>
where
M: TextRecognizable,
NerRecognizer: Recognizer<M> + '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)?;
Expand Down
4 changes: 1 addition & 3 deletions crates/nvisy-engine/src/analyzer/tabular.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Loading