Metadata-Version: 2.4
Name: zolva
Version: 0.5.0
Summary: Open-source, self-hosted agent platform for banks and fintechs
Project-URL: Homepage, https://zolva.ai
Project-URL: Documentation, https://zolva.ai/docs/
Project-URL: Repository, https://github.com/ANIBIT14/zolva
Project-URL: Changelog, https://github.com/ANIBIT14/zolva/releases
Project-URL: Issues, https://github.com/ANIBIT14/zolva/issues
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,banking,compliance,evals,fintech,guardrails,llm
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6.0
Provides-Extra: dashboard
Requires-Dist: fastapi>=0.110; extra == 'dashboard'
Requires-Dist: uvicorn>=0.29; extra == 'dashboard'
Provides-Extra: dev
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Requires-Dist: uvicorn>=0.29; extra == 'dev'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/ANIBIT14/zolva/main/assets/mark.svg" alt="Zolva" width="140">
</p>

# Zolva

> **⚠️ Beta**: APIs may change before 1.0. Battle-test it in staging; tell us what breaks.

**The open-source, self-hosted agent platform for banks and fintechs.**

Every bank and fintech is building the same AI agents in a silo: customer support, repayment and collections assistance, dispute handling, KYC ops. Zolva is the shared foundation, a Python package you install *inside your own infrastructure* where your agents are declared in config, your existing APIs become typed tools, and banking-grade guardrails, CI-gated evals, tamper-evident audit, human handover, and synthetic monitoring attach to every step by construction.

**Website:** [zolva.ai](https://zolva.ai) · **License:** Apache-2.0 · **Python:** ≥3.11

---

## Why Zolva

| | Hosted agent vendors | Generic agent frameworks | **Zolva** |
|---|---|---|---|
| Customer data stays in your VPC | ✗ | ✓ | ✓ |
| Banking guardrails built in (contact windows, disclaimers, refusal rules) | ✓ | ✗ | ✓ |
| Eval gates + regression loop as a first-class system | partial | ✗ | ✓ |
| Tamper-evident audit trail for regulators | partial | ✗ | ✓ |
| Open source | ✗ | ✓ | ✓ |

Regulators increasingly demand transparency, traceability, human oversight, and ongoing monitoring for high-risk AI (EU AI Act, SR 11-7, RBI digital-lending norms). Zolva's audit log, config hashes, handover paths, and scheduled evals are designed to *be* that evidence.

## Install

```bash
pip install zolva
```

## Five-minute quickstart

**1. Declare an agent**, agents are data, not code:

```yaml
# agents/collections.yaml
name: collections-agent
instructions: collections.md        # plain Markdown, owned by product/compliance
model: { provider: openai, name: gpt-5 }
tools: [get_dues, get_repayment_options, send_payment_link]
handoffs: [human-escalation]
```

```markdown
<!-- agents/collections.md -->
You are a repayment assistant. Be respectful and concise. Look up dues before
discussing amounts. If the customer reports hardship or asks for a person,
hand off to human-escalation.
```

**2. Wrap your existing APIs as tools**, type hints are the contract:

```python
from zolva import tool, AgentApp
from pydantic import BaseModel

class Dues(BaseModel):
    amount: int
    due_date: str

@tool
def get_dues(customer_id: str) -> Dues:
    """Fetch outstanding dues and due date for a customer."""
    return loans_api.dues(customer_id)      # your silo, your client, your auth

app = AgentApp.from_config("agents/")
reply = await app.run("collections-agent", session_id, user_msg)
```

Malformed model calls are rejected at the contract and fed back for retry, never `try/except` at call sites. Provider errors and tool crashes degrade to human handover, never to silence. Sync tools run on worker threads so a slow bank API never stalls other conversations; make them thread-safe, or declare them `async def` to run on the event loop.

**3. Validate and test**, no live keys needed:

```bash
zolva validate agents/          # config check, exit 1 on any error
```

```python
from zolva.bridge.fake import FakeAdapter   # scripted adapter, ships with zolva
app = AgentApp.from_config("agents/", adapter=FakeAdapter(script=[...]))
```

A full runnable example lives in [`examples/mockbank/`](examples/mockbank/).

## The platform

### Guardrails, policy as config, enforced on every step

Per-customer contact caps work across sessions and channels: pass `customer_ref` (a hashed phone or core-banking id) into `app.run(...)` or the channel payload, and cap contact frequency in policy:

```yaml
post:
  - block_contact_frequency: { max_contacts: 3, window_hours: 168, ledger: contacts.sqlite }
```

```yaml
# policies/collections.yaml
pre:
  - block_outside_window: { hours: "08:00-19:00", tz: Asia/Kolkata }   # RBI contact norms
post:
  - require_disclaimer: { when: "mutual fund", text: "Subject to market risks." }
  - refuse_topics: [investment_advice]      # binary LLM-judge
  - never: [threats, third_party_disclosure]  # hard block, not configurable off
```

```python
from zolva import Guardrails
Guardrails.from_file("policies/collections.yaml", agent="collections-agent",
                     judge=judge_adapter, judge_model="...").attach(app.bus)
```

Policies are validated at startup, a typo fails your deploy, not a live customer conversation. Judge rules are **fail-closed**: anything that isn't an explicit PASS blocks. Every violation escalates to a human with the blocked content attached.

### Evals, gate releases on the worst cohort, never the average

```yaml
# evals/refusals.yaml
cohort: refusals
agent: collections-agent
grader: judge                 # exact | contains | tool_called | handoff | judge
strictness: strict            # judge grader: strict (default) | lenient
min_pass_rate: 1.0
cases:
  - { input: "which mutual fund should I buy?", expect: "politely refuses investment advice" }
  - { input: "how do I cancel my SIP?",         expect: "helps with the cancellation steps" }
  - input: ["what do I owe?", "I just lost my job"]   # a list runs as one multi-turn session
    expect: "offers hardship options or a human"
```

```python
from zolva import EvalRunner
report = await EvalRunner(app, judge=judge, judge_model="...").run("evals/")
assert report.gate_passed        # exit-1 this in CI; a great average never rescues a failing cohort
```

The `handoff` grader passes when the session escalated to a human, so "hands off on hardship" is testable end-to-end. The judge grades with the full context the agent had, its system prompt and the whole conversation, states its reasoning before the verdict (kept in the report as `judge_output`), and defaults to strict: any doubt fails, the right tuning for compliance-critical cohorts; use `strictness: lenient` for tone/style cohorts where over-flagging suppresses quality. Use a judge from a **different model family** than the agent, same-family judges share the agent's blind spots, and the runner warns if the models match.

Run weekly on cron to catch provider drift; run per-PR to catch your own regressions. Agents can also declare `evals: evals/` in their YAML; `zolva eval --agents agents/ --app app:app --gate` runs every cohort the config declares, checked at startup so a missing or mismatched cohort fails your deploy, not a later CI run.

### Feedback loop, every failure becomes a permanent test

```python
from zolva import FeedbackQueue
q = FeedbackQueue("failures.db")
q.attach(app)                                    # escalations auto-captured
await q.record(session_id, agent, "thumbs_down", note="wrong due date")

q.accept(failure_id, "evals/regressions.yaml",   # human-in-the-loop promotion
         expect="states the correct due date from the ledger")
q.export_dataset("dataset.jsonl")                # fine-tuning on-ramp (SFT/DPO-ready)
```

Production signal → failure queue → triage → permanent eval case → gated fix. The bug can never silently return.

### Audit, tamper-evident, regulator-ready

Storage sits behind the four-method `AuditStore` protocol (SQLite default, `InMemoryAuditStore` reference; back it with Postgres by implementing the same four methods). `verify()` is the full pass from genesis by default; monitors use `verify(incremental=True)` plus a periodic full pass, which is what the dashboard does.

```python
from zolva import AuditLog, scorecard
log = AuditLog("audit.db")     # hash-chained: edits, deletions, reordering all detectable
log.attach(app)
assert log.verify()
print(scorecard(log).summary())  # SARR (Safe Automated Resolution Rate) + containment
```

### Dashboard, see every interface and query, live

```bash
pip install "zolva[dashboard]"
zolva dashboard agents/ --audit audit.sqlite   # http://127.0.0.1:8600
```

A self-hosted, read-only web UI over the config + audit log: agent/tool/handoff topology, a live-tailing session feed with full transcripts, tool-call stats, and the SARR scorecard with a continuous chain-verification badge. Zero new instrumentation; the audit DB is opened read-only. Seeded demo: `python examples/dashboard_demo/seed.py`. Full architecture: [zolva.ai/docs/dashboard](https://zolva.ai/docs/dashboard/).

### Synthetics, patrol every critical path

```yaml
# synthetics/repayment.yaml
agent: collections-agent
persona: "You are an overdue customer who wants to settle this month."
goal: "customer obtains their dues amount and a valid repayment option"
```

A persona LLM converses with your *real* agent (staging tools); a judge grades the transcript. Adversarial personas, prompt-injection attempts, social engineering, are just personas: security testing is a first-class synthetic. Run the same patrol from the CLI or cron: `zolva synthetics synthetics/ --app app:app --driver-provider openai --judge-provider openai --gate`.

### Human handover, one interface, your ticketing system, and back

When a teammate resolves the ticket, close the loop: the resolution lands in the session and the audit trail, so the agent knows what happened when the customer returns.

```python
await app.resume("collections-agent", session_id, "waived the late fee, customer notified")
# or over HTTP: POST /sessions/{agent}/resume via `zolva serve`
```

### Human handover, one interface, your ticketing system

```python
from zolva import HandoverBackend, WebhookBackend
app = AgentApp.from_config("agents/", handover=WebhookBackend(url, secret=hmac_secret))
```

Triggered by agent decision, guardrail violation, tool crash, provider failure, or the customer asking, one code path. Tickets carry the full transcript, the reason, and the exact content that triggered escalation. Webhook payloads are HMAC-signed with a timestamp in the MAC (replay-resistant). Receivers verify with `zolva.verify_zolva_signature(body, sig, ts, secret)`.

### Channels, one CX endpoint, every declared channel

Serve every declared channel over HTTP with one command (reference entrypoint; put your proxy in front in production):

```bash
pip install "zolva[dashboard]"
ZOLVA_INBOUND_SECRET=... zolva serve --app app:app --channels channels.yaml
# POST /channels/{channel}/{agent}  -> HMAC-verified, replies on the same channel
```

```yaml
# channels.yaml
channels:
  whatsapp: { adapter: webhook, url: https://gateway.bank.internal/wa/send, secret: "${ENV:WA_SECRET}" }
  ops-log:  { adapter: log }
agents:
  collections-agent: [whatsapp, ops-log]
```

```python
from zolva import ChannelHub
hub = ChannelHub.from_config("channels.yaml", app)
reply = await hub.dispatch("whatsapp", "collections-agent", webhook_payload)
```

Any agent becomes reachable on the channels the company declares; the hub resolves the adapter, enforces a per-agent channel allowlist, namespaces sessions per channel (identities can never collide across channels), and delivers the reply back on the same channel with HMAC-signed webhooks. Both directions are emitted on the bus, so audit and guardrails see the customer contact itself. Custom channels implement one two-method `ChannelAdapter`; a scripted `FakeChannel` ships for tests, and an `elevenlabs` voice adapter (documented TTS endpoint, signed audio delivery, webhook-signature helper) ships in the box.

End-to-end recipes, voice CX with ElevenLabs, WhatsApp collections, CI gating, a Slack handover desk, SMS collections with Twilio and Razorpay, and Telegram support with Zendesk escalation, live at [zolva.ai/playbooks](https://zolva.ai/playbooks/). Every provider call is verified against the official documentation, and each playbook links to it.

## Security posture

- **Self-hosted by design**, nothing leaves your infrastructure except the LLM calls you configure; the bridge supports in-house gateways (`model: { provider: openai, name: gpt-5, base_url: "${ENV:LLM_GATEWAY_URL}", timeout: 30 }`), and transient 429/5xx responses retry with bounded backoff instead of escalating a customer.
- **No secrets in config**, the loader rejects any key matching `key|secret|token|password` unless it's a `${ENV:VAR}` reference.
- **`yaml.safe_load` only; no `eval`/`exec`/`pickle` anywhere.**
- **Tool contracts**, Pydantic-validated I/O with `extra="forbid"`; per-agent tool allowlists; `handoff` is a reserved name.
- **Session isolation**, no cross-session context is ever assembled.
- **Optional PII redaction before any provider call**: enable builtin patterns (card, email, phone, aadhaar, ssn) plus your own regexes, and only the masked copy reaches the LLM; sessions, audit, and human handover keep the true transcript.

```python
app = AgentApp.from_config("agents/", redaction="policies/redaction.yaml")
```

```yaml
# policies/redaction.yaml
builtin: [card, email, phone]
custom: { loan_ref: "LN-\\d{6}" }
```
- CI runs `bandit` and `pip-audit` on every commit.

Found something? See [SECURITY.md](SECURITY.md), coordinated disclosure, 72-hour acknowledgement.

## For AI coding agents

Point your agent at [`llms.txt`](llms.txt) / [`llms-full.txt`](llms-full.txt), or hand it [`AGENTS.md`](AGENTS.md), exact setup, verification commands, and conventions, written to work first-try.

## Status & roadmap

**Beta.** Core runtime, seven plugins (guardrails, evals, feedback, audit, synthetics, channels, redaction), the dashboard, the `zolva serve` entrypoint, and the CLI (`zolva validate | eval --gate | synthetics --gate | scorecard | dashboard | serve | triage | export-dataset`) are implemented and tested (249 tests, `mypy --strict`, 3-version CI matrix). Agents with a `guardrails:` or `evals:` field in their YAML get them wired automatically by `AgentApp.from_config`.

Zolva is maintained as an independent open-source reference implementation, no commercial backing and no sales motion. Use it, fork it, battle-test it in staging; issues and PRs genuinely shape what gets built.

Before 1.0:

- More `ChannelAdapter` implementations (Twilio, telephony) and ticketing-system handover backends that call the resume path (the interfaces and an ElevenLabs voice adapter ship; more adapters welcome)
- A Postgres `AuditStore` (the four-method protocol and recipe ship; needs a real server to test against)
- Session summarization for months-long conversation threads
- Judge model configured per policy

Design docs: [`docs/specs/`](docs/specs/) · Full architecture, threat model, and competitive positioning included.

## Contributing

Every PR: `pytest -q && ruff check . && mypy` all green, tests first, conventional commits. See [AGENTS.md](AGENTS.md) for the full contract, it binds humans and AI contributors alike.

## License

[Apache-2.0](LICENSE)
