Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
AegisRoute

AegisRoute

Explainable, offline-first notification routing for multimodal message streams

Decide what interrupts a person — and be able to prove why.

License: Apache 2.0 Python 3.10+ Tests Adversarial Throughput Runtime cost Offline Code style: ruff PRs Welcome

AegisRoute — notify, digest, mute

Table of contents


Why this exists

A person receives a hundred messages a day. Their phone treats all of them the same, which produces two failures at once: important messages get buried, and unwanted or dangerous ones interrupt anyway.

Most triage systems answer "is this message important?" That is the wrong question. The right one is "is this message important to this person, right now — and is it safe?"

Those are two different problems with opposite requirements:

Preference Safety
Varies by person? Yes — deeply No — never
Learned from behaviour? Yes No
Overridable? Yes Never

AegisRoute separates them architecturally. An unconditional safety layer sits in front of a personalized preference layer, so a scam cannot be delivered because someone happens to trust the sender.


What it does

For every incoming message, AegisRoute emits one of three actions plus a written justification, a calibrated confidence, and citations to the user's own history:

Action Meaning
notify Interrupt now — concrete, time-bound, personally relevant
digest Safe and possibly useful — show it later
mute Low-value, repetitive, unwanted, or unsafe
$ aegisroute --dataset examples/sample_dataset explain demo_003

Message : demo_003  (user u_100, personal)
Text    : Please share your OTP here quickly to avoid account closure.

Decision: MUTE / scam @ 0.86
Reason  : The message asks for urgent OTP or account verification through a suspicious flow.
Evidence: none
Path    : deterministic

Reasoning trace
    asks the user to transmit a secret ['share your otp']
    = safety gate short-circuit -> mute (risk_locked)

Every decision is reproducible like this. No black boxes.


Quick start

git clone https://github.com/aegisroute/aegisroute.git
cd aegisroute
pip install -e .

# Route the bundled synthetic demo corpus
aegisroute --dataset examples/sample_dataset run --output demo_output.csv

Two dependencies (pandas, PyYAML). No API key, no GPU, no network.

Expected output
demo_001  notify  urgent            0.91  Work context with a direct deadline or meeting dependency.
demo_002  mute    greeting          0.84  Sender has a pattern of repeated forwards the user ignores.
demo_003  mute    scam              0.86  Asks for urgent OTP or account verification.
demo_004  notify  business_update   0.90  Verified business update matching recent order history.
demo_005  mute    promotion         0.88  User opted out of or dismissed similar marketing.
demo_006  digest  event             0.80  Useful group information, not urgent.
demo_007  mute    scam              0.86  Tries to instruct the router; judged on real content.
demo_008  digest  promotion         0.81  Matches known interests but is low priority.

Note demo_007: a prompt injection instructing the router to mark it notify. It is muted, and the attempt itself is treated as a risk signal.


Features

  • 🛡️ Unconditional safety gate — credential solicitation is muted regardless of sender trust or engagement history
  • 🎯 True personalization — identical text routes differently per user based on their own reaction history
  • 🔍 Full explainability — line-by-line reasoning trace for every decision
  • 🎙️ Multimodal — real OCR and speech-to-text, not just metadata
  • 🔌 Nine LLM providers — behind one interface, hot-swappable by config
  • 📴 Offline-first — complete functionality with zero credentials
  • 🎚️ Calibrated confidence — Brier 0.0215; admits uncertainty instead of asserting it
  • 🧪 209 tests including an 87-case adversarial red-team suite
  • 1,090 msg/s at 0.72 ms p50, single-threaded
  • 🔁 Deterministic — byte-identical output across runs, environments and cache states

Design philosophy

1. Safety is not a preference. No legitimate sender asks a user to transmit an OTP, PIN, CVV or password. Because that is categorically true, any rule that lets another signal soften it becomes an exploit path — and the obvious attack is to build familiarity first.

2. Interruption cost is asymmetric. A history of suppression is strong evidence to mute (weight 0.55). A history of engagement is only weak evidence to interrupt (weight 0.22). Opening a message is passive consumption, not a request to be alerted.

3. The LLM advises; it never decides. Language models are non-deterministic and jailbreakable. Putting one in a safety-critical path means an attacker can argue their way past it.

4. Measure decision impact, not component metrics. We benchmarked three OCR engines. The most accurate scored +22 points higher — and changed zero routing decisions. We kept the one that runs in 335 MB.

5. State limitations plainly. Every known weakness is documented. A defence that hides a limitation fails the moment someone finds it.


Architecture

flowchart TD
    A[Message stream<br/>CSV / API] --> B[Loader<br/>validate · index · integrity]
    M[Media files<br/>images · audio] --> C

    B --> C[MediaUnderstanding port]
    C --> C1[LiveAdapter<br/>RapidOCR + faster-whisper]
    C --> C2[CachedAdapter<br/>JSON replay]
    C1 --> D
    C2 --> D

    D[SignalExtractor<br/>~40 signals · 6 families] --> E{Decision Engine}

    E --> E1[1 · Safety Gate<br/>unconditional]
    E1 -->|risk found| LOCK[[risk_locked → mute]]
    E1 -->|clear| E2[2 · Classification<br/>ordered precedence]
    E2 --> E3[3 · Priority Scoring<br/>personalized]

    E3 --> F{LLM configured?}
    F -->|no| G
    F -->|yes| F1[LLM Advisor<br/>bounded arbitration]
    F1 --> G
    LOCK --> G

    G[Evidence · Reason · Confidence] --> H[Schema validation]
    H --> I[(output.csv<br/>+ decision trace)]

    style E1 fill:#fff5f5,stroke:#c92a2a,stroke-width:2px
    style LOCK fill:#ffe3e3,stroke:#c92a2a,stroke-width:2px
    style I fill:#e6fcf5,stroke:#2b8a3e,stroke-width:2px
Loading
Component diagram
graph LR
    subgraph io["io_ · boundaries"]
        L[loader.py]
        W[writer.py]
    end
    subgraph feat["features"]
        S[signals.py]
    end
    subgraph eng["engine"]
        R[rules.py]
        EV[evidence.py]
        RE[reasons.py]
        RS[reasoner.py]
    end
    subgraph llm["llm · provider port"]
        BA[base.py]
        PR[providers.py]
        RG[registry.py]
    end
    subgraph media["media"]
        MU[understanding.py]
    end
    subgraph util["util"]
        SEC[security.py]
        LOG[logging_setup.py]
    end

    P[pipeline.py] --> L
    P --> MU
    P --> S
    P --> R
    P --> EV
    P --> RE
    P --> RS
    P --> W
    RS --> RG
    RG --> PR
    PR --> BA
    MU --> SEC
    W --> SEC
Loading
Data flow
sequenceDiagram
    participant U as CLI
    participant P as Pipeline
    participant M as MediaUnderstanding
    participant S as SignalExtractor
    participant E as DecisionEngine
    participant L as LLM Advisor
    participant W as Writer

    U->>P: run --dataset ./data
    P->>P: load + index 13 tables
    loop each message
        P->>M: get(media_id)
        M-->>P: text (live | cached | unavailable)
        P->>S: extract(message, media)
        S-->>P: SignalSet (~40 signals)
        P->>E: decide(message, signals)
        alt safety gate triggers
            E-->>P: mute + risk_locked
            Note over L: LLM never consulted
        else clear
            E-->>P: action + score + trace
            P->>L: refine (optional)
            L-->>P: bounded verdict
        end
        P->>P: evidence + reason + confidence
    end
    P->>W: write + validate schema
    W-->>U: output.csv + trace.jsonl
Loading
Package dependency graph
graph TD
    CLI[cli.py] --> PIPE[pipeline.py]
    PIPE --> CFG[config.py]
    PIPE --> SCH[schemas.py]
    PIPE --> IO[io_]
    PIPE --> MED[media]
    PIPE --> FEA[features]
    PIPE --> ENG[engine]
    ENG --> LLM[llm]
    ENG --> FEA
    MED --> UTL[util]
    IO --> UTL
    LLM --> SCH

    style SCH fill:#eef2ff,stroke:#4c6ef5
    style UTL fill:#eef2ff,stroke:#4c6ef5
Loading

Dependencies point inward toward schemas and util. No cycles.


Decision engine

Three stages in strict order. Each appends to a human-readable trace.

flowchart LR
    IN[SignalSet] --> G{1 · Safety Gate}
    G -->|credential solicitation| M1[mute / scam<br/>LOCKED]
    G -->|prompt injection| M2[mute / scam or spam<br/>LOCKED]
    G -->|domain spoof + ask| M3[mute / scam<br/>LOCKED]
    G -->|clear| T[2 · Classify type]
    T --> SC[3 · Score]
    SC --> TH{Threshold}
    TH -->|score ≥ 0.65| N[notify]
    TH -->|score ≤ −0.71| MU[mute]
    TH -->|between| D[digest]

    style G fill:#fff5f5,stroke:#c92a2a,stroke-width:2px
    style M1 fill:#ffe3e3,stroke:#c92a2a
    style M2 fill:#ffe3e3,stroke:#c92a2a
    style M3 fill:#ffe3e3,stroke:#c92a2a
Loading

Signal families

Family Examples
Content urgency, action requests, events, payments, promos, greetings, deferral markers
Sender admin role, group type, business verification, domain match, account age
Relationship prior orders, opt-in state, opt-out timestamp, recent activity
Behaviour opened / replied / dismissed / muted / reported history
Risk credential solicitation, pressure language, unsafe flows, injection tactics
Load quiet hours, notification fatigue ratio

Scoring weights

Signal Δ
Type base (urgentspam) +0.85−0.85
Direct mention +0.35
Sender is admin +0.28
Matches recent business activity +0.30
Sender said it can wait −0.45
Opted out of promotions −0.55
User muted this group −0.45
Quiet hours −0.28
Suppression history × 0.55
Engagement history × 0.22

The asymmetry in the last two rows is the single most important parameter in the system. See DESIGN_DECISIONS.md.


Security model

flowchart TD
    UNTRUSTED[/"UNTRUSTED<br/>message text · OCR output · transcripts · file paths"/]

    UNTRUSTED --> L1[Layer 1 · Input validation<br/>path containment · size caps · type allowlists]
    L1 --> L2[Layer 2 · Injection detection<br/>22 patterns · 8 tactic families]
    L2 --> L3[Layer 3 · Neutralisation<br/>control chars · delimiters · length]
    L3 --> L4[Layer 4 · Structural fencing<br/>labelled data blocks in prompts]
    L4 --> L5[Layer 5 · Architectural bypass<br/>safety-locked decisions never reach the LLM]
    L5 --> SAFE[/"TRUSTED<br/>rules · thresholds · config"/]

    style UNTRUSTED fill:#fff5f5,stroke:#c92a2a,stroke-width:2px
    style L5 fill:#e6fcf5,stroke:#2b8a3e,stroke-width:2px
    style SAFE fill:#e6fcf5,stroke:#2b8a3e
Loading
Threat Control
Path traversal Canonical resolution + containment check; extension and size allowlists
Prompt injection Detection → treated as a risk signal → architectural bypass
Safety-gate jailbreak Credential solicitation is unconditional; LLM never sees these
CSV formula injection Output values escaped
PII leakage Redaction at the logging handler — covers every sink
Resource exhaustion Text caps, image downscaling, timeouts
Provider outage Retry → failover → offline engine

Layer 5 is the one that matters. Pattern matching is evadable — our own red team found a bypass using a payload that mimics the router's output schema. The architectural control does not depend on detection succeeding.

Full threat model: docs/SECURITY.md · Reporting: SECURITY.md


Media pipeline

flowchart LR
    F[media file] --> V{path safe?}
    V -->|no| X[reject + log]
    V -->|yes| C{in cache?}
    C -->|yes| R[return cached text]
    C -->|no| A{live deps available?}
    A -->|no| U[unavailable → metadata-only routing]
    A -->|yes| K{kind}
    K -->|image| O[RapidOCR<br/>downscale to 1000px]
    K -->|voice| S[faster-whisper base<br/>int8 CPU]
    O --> W[persist to cache]
    S --> W
    W --> R

    style X fill:#ffe3e3,stroke:#c92a2a
    style U fill:#fff9db,stroke:#e8590c
Loading

Every degradation step is logged, never silent.

Voice notes carry no text at all — the transcript is the message. A metadata-only system is blind to them.

Engine selection is documented with measurements in docs/OCR.md and docs/SPEECH.md.


LLM providers

Nine adapters behind one LLMProvider interface. Switching is a config change.

flowchart TD
    APP[Pipeline] --> PORT[[LLMProvider interface]]
    PORT --> REG[Registry<br/>discovery · priority · failover]

    REG --> H1[OpenAI]
    REG --> H2[Anthropic]
    REG --> H3[Gemini]
    REG --> H4[Groq]
    REG --> H5[OpenRouter]
    REG --> L1[Ollama]
    REG --> L2[LM Studio]
    REG --> L3[vLLM]
    REG --> L4[Any OpenAI-compatible]
    REG --> GW[LiteLLM gateway<br/>optional]

    REG -.->|all unavailable| OFF[[Deterministic engine]]

    style PORT fill:#eef2ff,stroke:#4c6ef5,stroke-width:2px
    style OFF fill:#e6fcf5,stroke:#2b8a3e,stroke-width:2px
Loading

Resolution order: explicitly selected → first configured hosted → responding local → deterministic offline engine.

Built in: capability detection, retry with exponential backoff, typed error taxonomy (auth errors never retried, rate limits always are), structured-JSON coercion with malformed-output repair, token and cost accounting, cross-provider failover.

Arbitration policy

The deterministic engine runs first. The LLM sees its verdict and may refine it only within these bounds:

Rule Behaviour
Decision is safety-locked LLM is not called at all
Override the action Requires confidence ≥ 0.72
notifymute flip Blocked under safety lock
Escalate into a risk type Always permitted
Malformed / illegal response Silently rejected; rule verdict stands

Details: docs/PROVIDERS.md


Offline mode

This is the default, not a fallback.

aegisroute --dataset ./data run --no-llm
Property Offline With LLM
Output produced ✅ complete ✅ complete
Cost $0.00 see cost table
Latency 0.72 ms/msg + network
Determinism guaranteed best-effort
Safety decisions rule engine rule engine (LLM bypassed)
Data leaves machine never prompts sent to provider

Verified with a deliberately invalid API key: the system hit a real endpoint, received AuthenticationError, failed over, degraded to the deterministic engine, and produced byte-identical output.


Benchmarks

Measured on 2 vCPU / 1,985 MB RAM, CPU-only. Reproduce with python scripts/benchmark.py.

Metric Value
Throughput (warm) 1,090 msg/s
Latency p50 / p90 / p99 0.72 ms / 1.23 ms / 5.88 ms
Warm run 0.82 s
Cold run (live OCR + STT) 56.0 s
Cache speedup 68×
Peak RSS warm / cold < 200 MB / 1,121 MB
Determinism across 3 runs byte-identical

OCR engine comparison

Engine Keyword recall Wall time Peak RSS Install Routing changes
RapidOCR 0.713 32 s 335 MB 72 MB baseline
EasyOCR 0.842 143 s 1,468 MB 4,880 MB 0
PaddleOCR 0.931 347 s 1,514 MB ~420 MB 0

A 22-point accuracy gain changed zero decisions. EasyOCR pulls the entire CUDA stack (2.7 GB) onto a GPU-less machine; PaddleOCR was OOM-killed.

Speech model comparison

Model Wall time Realtime Peak RSS Routing changes
tiny 27.5 s 8.8× 804 MB 1
base 45.9 s 5.3× 896 MB baseline
small 183.3 s 1.3× 1,508 MB 0

base is the smallest model that transcribes every safety-critical token correctly. small renders OTP as OTT on a bank-scam clip.

Full analysis: docs/BENCHMARKS.md


Cost comparison

Per 1,000 messages, assuming ~55% reach the LLM (safety-locked decisions bypass it entirely — a security control that also cuts spend by 45%).

Provider Model Cost / 1k msgs Latency
None (default) deterministic $0.00 0.0009 s
Gemini 2.0 Flash $0.09 ~0.8 s
OpenAI gpt-4o-mini $0.13 ~1.0 s
Groq llama-3.3-70b $0.43 ~0.3 s
Anthropic Claude 3.5 Haiku $0.76 ~0.9 s
Anthropic Claude 3.5 Sonnet $2.88 ~2.0 s
Self-hosted vLLM ~$310/mo 1–2 s (GPU)

Break-even for self-hosting vs Gemini Flash: ~3.5M messages/month. Full analysis: docs/PROVIDERS.md


Installation

Requirements

  • Python 3.10+ (uses dataclass(slots=True))
  • No GPU, no root, no network required

From source

git clone https://github.com/aegisroute/aegisroute.git
cd aegisroute
pip install -e .                 # core only
pip install -e ".[media]"        # + OCR and speech-to-text
pip install -e ".[llm]"          # + LiteLLM gateway
pip install -e ".[all]"          # everything including dev tools

Dependency matrix

Extra Adds Without it
core pandas, PyYAML — required
media RapidOCR, faster-whisper, Pillow, numpy media routed on metadata only
llm LiteLLM native adapters still work
dev pytest, ruff, mypy, radon tests cannot run

CLI usage

aegisroute --dataset ./data run                 # route and write output.csv
aegisroute --dataset ./data run --no-llm        # force deterministic
aegisroute --dataset ./data run --provider ollama
aegisroute --dataset ./data validate            # check output schema
aegisroute --dataset ./data evaluate            # score against labelled samples
aegisroute --dataset ./data explain <id>        # full reasoning trace
aegisroute providers                            # list detected LLM providers
Flag Purpose
--dataset PATH Input directory
--config PATH Config file (default config.yaml)
--output PATH Output CSV path
--no-llm Deterministic engine only
--provider NAME Pin a provider
--media-adapter auto · live · cached · null
-v, --verbose Debug logging

Library use

from pathlib import Path
from aegisroute.config import load_config
from aegisroute.pipeline import RouterPipeline

cfg = load_config(Path("config.yaml"), Path("."))
cfg["llm"]["enabled"] = False

pipeline = RouterPipeline(cfg, Path("."))
for message in pipeline.ds.messages:
    decision = pipeline.route_one(message)
    print(decision.action, decision.message_type, decision.confidence)
    for line in decision.signals["trace"]:
        print("   ", line)

Configuration

All behaviour lives in config.yaml. Nothing is hardcoded.

engine:
  notify_threshold: 0.65        # plateau centre from a 2-D sweep
  mute_threshold: -0.71
  quiet_hours_penalty: 0.28
  notification_fatigue_penalty: 0.18

confidence:                     # mirrors observed ground-truth calibration
  notify: {base: 0.87, min: 0.85, max: 0.91}
  digest: {base: 0.81, min: 0.78, max: 0.85}
  mute:   {base: 0.83, min: 0.81, max: 0.88}

llm:
  provider: auto                # or openai | anthropic | gemini | groq | ...
  arbitration:
    llm_override_min_confidence: 0.72
    safety_lock: true           # scam/spam mutes are never overridden

Override anything from the environment:

export AEGISROUTE_ENGINE__NOTIFY_THRESHOLD=0.70
export AEGISROUTE_PROVIDER=groq

Environment variables

Variable Purpose
AEGISROUTE_PROVIDER Pin a provider
AEGISROUTE_MODEL Override the model
AEGISROUTE_<SECTION>__<KEY> Override any config value
OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY Hosted providers
GROQ_API_KEY / OPENROUTER_API_KEY Hosted providers
OLLAMA_BASE_URL / LMSTUDIO_BASE_URL / VLLM_BASE_URL Local servers

Secrets are read only from the environment. Copy .env.example to .env.


Docker

docker build -t aegisroute .
docker run --rm -v "$PWD/data:/data" aegisroute --dataset /data run

With a provider:

docker run --rm -v "$PWD/data:/data" -e GEMINI_API_KEY="$GEMINI_API_KEY" \
  aegisroute --dataset /data run

Development

pip install -e ".[all]"

pytest                      # 209 tests, ~4s
pytest tests/test_adversarial.py -v   # 87 red-team cases
ruff check src tests        # lint
mypy src                    # type check
radon cc src -s -n C        # complexity
python scripts/benchmark.py # performance

Test suites

Suite Tests Focus
test_security.py 32 Traversal, injection, redaction, CSV escaping
test_engine.py 30 Safety gate, classification, personalization
test_llm.py 33 Registry, retry, failover, arbitration
test_pipeline.py 27 Loading, media, contract, determinism
test_adversarial.py 87 Red team

See CONTRIBUTING.md and docs/CODE_STYLE.md.


Project structure

aegisroute/
├── src/aegisroute/
│   ├── cli.py              Command-line interface
│   ├── config.py           YAML + .env + env-var precedence
│   ├── pipeline.py         Orchestration
│   ├── schemas.py          Typed domain models
│   ├── evaluate.py         Scoring harness
│   ├── engine/             Safety gate, classification, scoring, evidence
│   ├── features/           Signal extraction (~40 signals, 6 families)
│   ├── llm/                Provider port, 9 adapters, registry
│   ├── media/              OCR + speech-to-text adapters
│   ├── io_/                Loading, validation, writing
│   └── util/               Security primitives, structured logging
├── tests/                  209 tests
├── docs/                   Architecture, security, benchmarks, decisions
├── prompts/                LLM prompt templates
├── examples/               Synthetic demo corpus
├── assets/                 Logo, banner, social preview
└── scripts/                Benchmark and report tooling

Why io_ has a trailing underscore: to avoid shadowing the stdlib io module.


FAQ

Why rules instead of a fine-tuned model?

Three reasons. Rules are auditable — you can read exactly why a message was muted. They are deterministic, so the same input always yields the same output. And they cannot be jailbroken by text in the message itself.

The LLM layer exists precisely to add the semantic generalisation rules lack — but as an advisor, never as the authority.

Can I use this without any AI provider?

Yes — that is the default. The deterministic engine produces complete output with zero credentials, zero cost and guaranteed reproducibility.

How do I adapt it to my own message schema?

Implement a loader that produces Message objects (see schemas.py) and point the pipeline at it. The engine only consumes SignalSet, so the input format is entirely swappable.

Is the lexicon approach multilingual?

Partially. Romanised Hindi scam patterns are included because they appeared in our evaluation corpus. Coverage is not systematic across languages — this is a known limitation, tracked in the roadmap. Lexicons are data, not code, so adding a language does not require touching the engine.

Why is confidence capped at 0.91?

Because calibration matters more than confidence theatre. Emitting 0.99 everywhere looks assured and scores badly on any proper scoring rule. Our Brier score is 0.0215.

How is this different from a spam filter?

A spam filter answers a binary, global question: is this junk? AegisRoute answers a three-way, per-person, time-aware one: should this interrupt this user right now? A legitimate sale poster is not spam — but it is mute for someone who has dismissed six similar posts and digest for someone who buys from that group.


Troubleshooting

FileNotFoundError: required dataset file missing

Point --dataset at a directory containing messages.csv:

aegisroute --dataset examples/sample_dataset run
Media not being processed

Install the extras and check adapter selection:

pip install -e ".[media]"
aegisroute --dataset ./data run -v 2>&1 | grep "media adapter"

mode=cached means live dependencies were not importable.

Out of memory during OCR

Lower the downscale ceiling in config.yaml:

media:
  ocr:
    max_side_px: 640
Provider not detected
aegisroute providers

Local providers must actually be answering — the registry probes them and skips anything unreachable.

TypeError: dataclass() got an unexpected keyword argument 'slots'

You are on Python 3.9. AegisRoute requires 3.10+.


Roadmap

Version Theme Highlights
v1.1 Hardening Lexicons to YAML · parallel media · embedding-based evidence retrieval
v2.0 Learning & scale Continual learning · memory engine · RAG · distributed workers
v2.5 Real-time & edge Streaming ingestion · on-device inference · federated learning
v3.0 Platform Policy engine · dashboard · analytics · observability

Two things will never change: the safety gate stays rule-based (a learned one can be poisoned), and offline capability stays a first-class mode.

Full plan: docs/ROADMAP.md


Contributing

Contributions are welcome. Start with good first issues.

  1. Read CONTRIBUTING.md and docs/CODE_STYLE.md
  2. Fork, branch, and add tests — safety changes require adversarial tests
  3. Run pytest && ruff check src tests && mypy src
  4. Open a PR describing the trade-off you chose and what you rejected

By participating you agree to the Code of Conduct.


License

Apache License 2.0 — free for commercial and private use, with an explicit patent grant.


Acknowledgements

Design informed by published work and production systems:

  • Gmail Priority Inbox (Aberdeen et al., 2010) — per-user thresholding
  • Android Notification Channels — dismissal as the strongest negative signal
  • Apple Focus / Time Sensitive — raising the interruption bar rather than blocking
  • Greshake et al., 2023 — indirect prompt injection via retrieved content
  • Guo et al., 2017 — confidence calibration and proper scoring rules
  • Pielot et al., 2014 — notification fatigue

Built on RapidOCR, faster-whisper, and LiteLLM.

Built for people whose attention is worth protecting.

About

offline-first notification routing for multimodal message streams

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages