Metadata-Version: 2.4
Name: notilens
Version: 0.5.5
Summary: NotiLens — send alerts to NotiLens from Python scripts, apps, and AI agents
Author: NotiLens
License-Expression: MIT
Project-URL: Homepage, https://www.notilens.com
Project-URL: Documentation, https://www.notilens.com/doc
Keywords: notilens,ai-agent,notifications,monitoring,observability,cli
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == "openai"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.20; extra == "anthropic"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.1; extra == "langchain"
Provides-Extra: crewai
Requires-Dist: crewai>=0.28; extra == "crewai"
Provides-Extra: pydantic
Requires-Dist: pydantic-ai>=0.0.9; extra == "pydantic"
Provides-Extra: http
Requires-Dist: httpx>=0.24; extra == "http"
Requires-Dist: requests>=2.28; extra == "http"
Provides-Extra: all
Requires-Dist: openai>=1.0; extra == "all"
Requires-Dist: anthropic>=0.20; extra == "all"
Requires-Dist: langchain-core>=0.1; extra == "all"
Requires-Dist: crewai>=0.28; extra == "all"
Requires-Dist: pydantic-ai>=0.0.9; extra == "all"
Requires-Dist: httpx>=0.24; extra == "all"
Requires-Dist: requests>=2.28; extra == "all"

# NotiLens

Send alerts to NotiLens from Python scripts, apps, and AI agents.

Two ways to use it — pick one or both:

- **CLI** — for shell scripts, Claude Code hooks, bash pipelines
- **SDK** — for Python projects, with optional AI framework auto-patching

---

---

# CLI

Use the CLI in shell scripts, Claude Code hooks, or any terminal workflow.

## 1. Setup (required, one time)

Get your token and secret from the [NotiLens dashboard](https://www.notilens.com).

```bash
notilens init --name my-app --token YOUR_TOKEN --secret YOUR_SECRET
```

This saves credentials to `~/.notilens_config.json`. All future commands for this agent read from there — no need to pass token/secret again.

**Multiple sources** (each notifies a different topic):
```bash
notilens init --name scraper --token TOKEN_A --secret SECRET_A
notilens init --name mailer  --token TOKEN_B --secret SECRET_B
```

---

## 2. Notify

The simplest way to send a notification — no task or run context needed:

```bash
notilens notify order.placed    "Order #1234"      --name my-app
notilens notify disk.space.full "Only 2GB left"    --name my-app --level warning
notilens notify report.ready    "Report is ready"  --name my-app --download_url https://example.com/report.pdf
```

---

## 3. Commands

`--task` is a semantic label (e.g. `email`, `report`). Each `task.start` creates an isolated run internally — concurrent executions of the same label never conflict.

### Task Lifecycle

```bash
notilens queue    --name my-app --task email
notilens start    --name my-app --task email
notilens progress "Fetching data"  --name my-app --task email
notilens loop     "Step 3 of 10"   --name my-app --task email
notilens retry    --name my-app --task email
notilens pause    "Rate limited"   --name my-app --task email
notilens resume   "Resuming"       --name my-app --task email
notilens wait     "Awaiting tool"  --name my-app --task email
notilens stop     --name my-app --task email
notilens complete "All done"       --name my-app --task email
notilens error    "Step 3 failed"  --name my-app --task email
notilens fail     "Unrecoverable"  --name my-app --task email
notilens timeout  "Took too long"  --name my-app --task email
notilens cancel   "User cancelled" --name my-app --task email
notilens terminate "Out of memory" --name my-app --task email
```

`task.start` prints the internal `run_id` to stdout. You can capture it if needed — but for sequential scripts, just use `--task LABEL` and the SDK handles the rest automatically.

### Input / Human-in-the-loop

```bash
notilens input.required "Please confirm the output" --name my-app --task email
notilens input.approve  "Confirmed"                 --name my-app --task email
notilens input.reject   "Rejected by user"          --name my-app --task email
```

### Output Events

```bash
notilens output.generate "Report ready"     --name my-app --task email
notilens output.fail     "Model unavailable" --name my-app --task email
```

### Metrics

Pass any key=value pairs — numeric values accumulate across calls:

```bash
notilens metric tokens=512 cost=0.003 --name my-app --task email
notilens metric records=1500          --name my-app --task email

# Reset one metric
notilens metric.reset tokens --name my-app --task email

# Reset all metrics
notilens metric.reset --name my-app --task email
```

### Custom Events

```bash
notilens track user.registered "New signup"      --name my-app
notilens track disk.space.full "Only 2GB left"   --name my-app
notilens track order.placed    "Order #1234"      --name my-app
```

---

## 4. Full CLI Example

```bash
# Register once
notilens init --name summarizer --token my_token --secret my_secret

# Run a job
notilens start --name summarizer --task report

notilens metric tokens=1024 --name summarizer --task report
notilens metric cost=0.004  --name summarizer --task report

notilens complete "Summary ready" \
  --name summarizer \
  --task report \
  --open_url https://example.com/summary.pdf \
  --meta pages=12
```

---

## 5. Claude Code Hooks Example

Register the agent once:
```bash
notilens init --name claude-code --token YOUR_TOKEN --secret YOUR_SECRET
```

Then in `~/.claude/settings.json`:
```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "notilens progress \"Using tool: $CLAUDE_TOOL_NAME\" --name claude-code --task $CLAUDE_SESSION_ID"
      }]
    }],
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "notilens complete \"Session ended\" --name claude-code --task $CLAUDE_SESSION_ID"
      }]
    }]
  }
}
```

---

## CLI Options

| Flag | Required | Description |
|------|----------|-------------|
| `--name NAME` | Yes | Name identifying the source (agent, app, service, etc.) |
| `--task LABEL` | For task commands | Task label (semantic name, e.g. `email`, `report`) |
| `--level` | No | Override level: `debug` `info` `warning` `error` |
| `--meta key=value` | No | Custom metadata (repeatable) |
| `--image_url URL` | No | Attach an image |
| `--open_url URL` | No | Link to open |
| `--download_url URL` | No | Link to download |
| `--tags "tag1,tag2"` | No | Comma-separated tags |
| `--is_actionable true\|false` | No | Override actionable flag |
| `--force_send true\|false` | No | Bypass ML filtering. Default `true` for `notify`, `fail`, `timeout`, `terminate`, `input.required`, `output.*` |

---

---

# SDK

Use the SDK in Python projects. Supports manual task lifecycle calls and optional auto-patching of AI frameworks.

## 1. Setup (required)

```python
import notilens

# token/secret can also come from NOTILENS_TOKEN / NOTILENS_SECRET env vars
nl = notilens.init(
    name="my-app",    # required — name identifying this source
    token="YOUR_TOKEN",  # required — or set NOTILENS_TOKEN env var
    secret="YOUR_SECRET" # required — or set NOTILENS_SECRET env var
)
```

**Via environment variables:**
```bash
export NOTILENS_TOKEN=your_token
export NOTILENS_SECRET=your_secret
```
```python
nl = notilens.init(name="my-app")  # reads token+secret from env
```

**All init options:**
```python
nl = notilens.init(
    name="my-app",      # required
    token="...",           # required (or env var)
    secret="...",          # required (or env var)
    patch=False,           # optional — auto-patch AI frameworks (default: False)
    state_ttl=86400,       # optional — orphaned state TTL in seconds (default: 86400 / 24h)
    min_level="info",      # optional — minimum event level to send (default: "info")
    call_timeout=30.0,     # optional — alert if AI call exceeds N seconds (default: 30)
    debug=False,           # optional — verbose logging (default: False)
)
```

---

## 2. Notify

The simplest way to send a notification — no task or run context needed:

```python
nl.notify("order.placed", "Order #1234")
nl.notify("disk.space.full", "Only 2GB left", level="warning")
nl.notify("report.ready", "Your report is ready",
    download_url="https://example.com/report.pdf",
    meta={"pages": 12},
    tags="report,weekly",
)
# force_send=True by default — pass False to route through ML
nl.notify("low.priority", "FYI only", force_send=False)
```

Also available on a run, without any run state attached:

```python
run.notify("deploy.done", "Deployed to production",
    open_url="https://example.com/deploy/123",
)
```

---

## 3. Task Lifecycle

`nl.task(label)` creates a `Run` — an isolated execution context with its own state. Multiple concurrent runs of the same label never conflict.

```python
run = nl.task("email")     # create a run for the "email" task
run.queue()                    # optional — pre-start signal
run.start()                    # begin the run

run.progress("Fetching data")  # mid-run update
run.loop("Processing item 42") # loop iteration marker
run.retry()                    # retry signal

# Pause / resume / wait (non-terminal)
run.pause("Rate limited")
run.resume("Resuming work")
run.wait("Waiting for tool response")

run.stop()                     # non-terminal stop

# Non-terminal error (run continues)
run.error("Step 3 failed, retrying")

# Terminal events — pick one to end the run
run.complete("All done")
run.fail("Unrecoverable error")
run.timeout("Exceeded time limit")
run.cancel("User cancelled")
run.terminate("OOM")
```

---

## 4. Input / Human-in-the-loop

```python
run.input_required("Confirm before proceeding")
run.input_approved("User confirmed")
run.input_rejected("User rejected")
```

---

## 5. Output Events

```python
run.output_generated("Summary ready")
run.output_failed("Model unavailable")
```

---

## 6. Metrics

Track any numeric or string values per run — accumulated automatically and included in every notification.

```python
run.metric("tokens", 350)    # set
run.metric("tokens", 210)    # now 560 (numeric values accumulate)
run.metric("cost", 0.0012)
run.metric("records", 1500)
run.metric("model", "gpt-4") # strings are replaced, not accumulated

run.reset_metrics("tokens")  # reset one metric
run.reset_metrics()           # reset all metrics
```

---

## Automatic Timing

NotiLens automatically tracks task timing. These fields are included in every notification's `meta` payload when non-zero:

| Field | Description |
|-------|-------------|
| `total_duration_ms` | Wall-clock time since `start` |
| `queue_ms` | Time between `queue` and `start` |
| `pause_ms` | Cumulative time spent paused |
| `wait_ms` | Cumulative time spent waiting |
| `active_ms` | Active time (`total − pause − wait`) |

---

## force_send

By default NotiLens routes notifications through ML-based filtering and relevance ranking. Set `force_send=True` to bypass ML and deliver immediately to the user.

Events/methods that default to `force_send=True` (high-signal events):

| CLI command | SDK method | Default |
|-------------|------------|---------|
| `notilens notify` | `notify()` | `True` |
| `notilens fail` | `fail()` | `True` |
| `notilens timeout` | `timeout()` | `True` |
| `notilens terminate` | `terminate()` | `True` |
| `notilens input.required` | `input_required()` | `True` |
| `notilens output.generate` | `output_generated()` | `True` |
| `notilens output.fail` | `output_failed()` | `True` |
| Everything else | Everything else | `False` |

All commands and methods accept `force_send` as an overridable parameter:

**CLI:**
```bash
# Override to go through ML
notilens fail "Error" --name my-app --task email --force_send false

# Override to bypass ML
notilens progress "Critical step" --name my-app --task email --force_send true
```

**SDK:**
```python
# Override a default-True method to go through ML
run.fail("Unrecoverable error", force_send=False)

# Override a default-False method to bypass ML
run.progress("Critical checkpoint", force_send=True)
```

---

## 7. Custom Events

```python
run.track("user.registered", "New signup", meta={"plan": "pro"})  # meta optional
run.track("disk.space.full", "Only 2GB left", level="warning")    # level optional
run.track("order.placed", "Order #1234", meta={"amount": 99.99})
```

---

## 8. Auto-patching AI Frameworks

Add `patch=True` to `init()` — no other changes needed. NotiLens will automatically track every AI call.

```python
import notilens
import openai  # or anthropic, langchain, crewai, pydantic-ai

nl = notilens.init(
    name="my-app",
    token="YOUR_TOKEN",
    secret="YOUR_SECRET",
    patch=True,           # required to enable auto-patching
    call_timeout=30.0,    # optional — alert if any AI call takes longer than 30s
)

# From here, use OpenAI / Anthropic etc. normally.
# NotiLens fires ai.call.start, ai.call.complete, task.error, task.timeout, task.loop automatically.
response = openai.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarise this..."}],
)
```

**Multiple agents — only one can own patching:**
```python
scraper = notilens.init(name="scraper", token="TOKEN_A", secret="SECRET_A", patch=True)
mailer  = notilens.init(name="mailer",  token="TOKEN_B", secret="SECRET_B")
# patch=True on a second agent raises RuntimeError
```

---

## 9. Full SDK Example

```python
import notilens

nl = notilens.init("summarizer", token="my_token", secret="my_secret")
run   = nl.task("report")
run.start()

try:
    run.progress("Fetching PDF")

    result = llm.complete(prompt)
    run.metric("tokens", result.usage.total_tokens)
    run.metric("cost", result.usage.cost)

    run.output_generated("Summary ready")
    run.complete("All done")

except Exception as e:
    run.fail(str(e))
```

---

---

# Events Reference

| Event | Default Type | Description |
|-------|-------------|-------------|
| `task.queued` | info | Task queued |
| `task.started` | info | Task began |
| `task.progress` | info | Mid-run update |
| `task.loop` | warning | Loop iteration |
| `task.retry` | warning | Retry attempt |
| `task.completed` | success | Task finished successfully |
| `task.stopped` | info | Manually stopped |
| `task.failed` | urgent | Task failed |
| `task.error` | urgent | Non-fatal error |
| `task.timeout` | urgent | Exceeded time limit |
| `task.cancelled` | warning | Task cancelled |
| `task.terminated` | urgent | Force-terminated |
| `task.paused` | warning | Task paused |
| `task.resumed` | info | Task resumed |
| `task.waiting` | warning | Waiting for external response |
| `output.generated` | success | Output produced (AI response, report, file, etc.) |
| `output.failed` | urgent | Output generation failed |
| `input.required` | warning | Waiting for human input |
| `input.approved` | success | Input approved |
| `input.rejected` | warning | Input rejected |

---

## Requirements

- Python >= 3.9

## License

MIT — [notilens.com](https://www.notilens.com)
