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 examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ service keys.
| [Build a multi-modal product classifier with embeddings](./taxonomy-classification) | Evaluating text, image, NLI, and reranking approaches for hierarchical product taxonomy classification | `extract`, `encode`, `score` | SIE endpoint, Shopify dataset prep via `uv run` scripts, standalone `uv` project | Runnable evaluation example |
| [Swap an OCR model with one identifier change](./document-ocr) | Driving recognition (VLM-OCR), structured extraction (Donut), and zero-shot NER (GLiNER) through the same `extract` call by swapping the model ID | `extract` | Docker Compose plus Node UI, no API key required, hosted version on [Hugging Face Spaces](https://huggingface.co/spaces/superlinked/document-ocr) | Runnable demo |
| [A Stripe Link checkout with an SIE fraud-risk gate](./stripe-link-fraud) | Wiring all three SIE primitives into a pre-authorization fraud-risk gate that runs in the same round-trip as the Stripe PaymentIntent | `extract`, `encode`, `score` | Docker Compose plus Node UI; Stripe test-mode keys optional (runs in mock mode without them) | Runnable demo |
| [Turn account signals into one score with SIE ranking](./account-signal-scoring) | Rolling a pile of CRM signals into one churn/expansion score, then ranking each account's story against a corpus of past-outcome playbooks to pick the recommended play | `extract`, `encode`, `score`, `chat/completions` | Docker Compose plus Node UI, CPU-only by default; `SIE_CHAT_MODEL` optional for the LLM brief | Runnable demo |
| [Vision-first document RAG](./vision-doc-rag) | Retrieving and answering questions over a multi-tenant page corpus by looking at page images — including scanned drawings — with OCR kept out of the score path | `encode`, `chat/completions`, `score` (optional) | GPU SIE deployment required: ColQwen2.5 retriever + Qwen3.5-4B answer model (runs on the generation bundle) | Runnable demo |
| [Multi-model contract review with the OpenAI Agents SDK](./contract-review-agent) | Running an OpenAI Agents SDK agent whose every model call — triage, orchestration, vision, OCR, embeddings, rerank, entity extraction, text-to-SQL, reasoning, and a safety guardrail — is served by one SIE cluster, each step on the right catalog model, with per-model observability | `chat/completions`, `encode`, `score`, `extract` | GPU SIE deployment required; standalone `uv` project; real contracts fetched from CUAD (CC BY 4.0) | Runnable demo |

Expand Down
14 changes: 14 additions & 0 deletions examples/account-signal-scoring/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SIE
SIE_URL=http://localhost:8080
SIE_API_KEY=

# Optional: LLM account brief.
# Leave blank and the demo writes a deterministic brief from the account's
# signals (CPU-only, no generation model needed). Set SIE_CHAT_MODEL to a
# generation model your SIE cluster serves (e.g. Qwen/Qwen3-4B-Instruct-2507,
# which needs the generation bundle / a GPU) to have SIE draft the brief via
# the OpenAI-compatible /v1/chat/completions endpoint instead.
SIE_CHAT_MODEL=

# UI server
PORT=3044
4 changes: 4 additions & 0 deletions examples/account-signal-scoring/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.env
*.log
data/playbook_index.json
177 changes: 177 additions & 0 deletions examples/account-signal-scoring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# account-signal-scoring

Turn a pile of account signals into **one score a team can act on** — with SIE
doing the ranking.

A CSM starts the day staring at a mess of signals: a Stripe downgrade here, an
API-usage cliff there, a seat cap about to hit somewhere else. This demo
collapses that into a single triage board. Each account's signals roll up into
one 0-100 score, and SIE ranks the account's *story* against a corpus of
past-outcome **playbooks** to pick the recommended play — then optionally drafts
the account brief with an LLM.

It uses three SIE primitives — `extract`, `encode`, and `score` — in one
round-trip, plus an optional `chat/completions` call for the brief. Everything
runs locally on CPU with `docker compose`; the LLM brief is opt-in.

> Contributed from a `{Tech: Europe}` London AI Hackathon project (codename
> "Rick") by the Attio-integration team. A cleaned-up, self-contained slice of a
> larger CRM churn-rescue + expansion product.

## What this demo actually shows

1. **Signals → one score (deterministic).** Each signal has a direction (risk
vs. opportunity) and a severity weight. They roll up into a single score and
a red/amber/green band. This part is plain, auditable arithmetic — you always
know *why* an account is where it is.
2. **The account's story → the right play (SIE).** The score alone doesn't tell
you what to *do*. So the account context is written as prose, `encode`d, and
ranked (cosine → cross-encoder `score`) against a corpus of past outcomes
("champion left → renewed with new champion", "seat cap → expansion signed").
The top-matched playbook drives the recommended play.
3. **A brief a human can send (optional LLM).** With `SIE_CHAT_MODEL` set, SIE
drafts a summary / drivers / recommended-play brief via the OpenAI-compatible
`/v1/chat/completions` endpoint, grounded in the matched playbook. Without it,
a deterministic brief is written from the account's own data — so the board is
always populated.

The UI streams every stage over SSE so you can watch extract → encode → score →
brief happen live.

## Run it locally

Requires Docker and Node 22+.

```bash
cd examples/account-signal-scoring
npm install
cp .env.example .env # optional; defaults work out of the box
npm start # docker compose up -d + the UI server
```

`npm start` boots a local SIE server (CPU image, ~440 MB of models) and opens
the UI on <http://localhost:3044>. The first request builds the playbook index
(`data/playbook_index.json`) by encoding the corpus once; subsequent runs reuse
it.

Prefer the terminal? Rank every account into one board without the UI:

```bash
npm run score # score all accounts, print the ranked boards
npm run score helix # score a single account, verbose
```

### Turning on the LLM brief

The brief is deterministic by default so the demo stays CPU-only. To have SIE
draft it instead, point `SIE_CHAT_MODEL` at a generation model your cluster
serves (this needs the SIE generation bundle / a GPU — it is *not* in the
CPU compose file):

```bash
# .env
SIE_CHAT_MODEL=Qwen/Qwen3-4B-Instruct-2507
```

The account panel header shows which mode you're in (`brief: deterministic` vs.
`brief: LLM (...)`).

## Specific things to try in the UI

| Account | Signals | Expected |
|---|---|---|
| **Pemberton & Co** | Stripe downgrade staged + NPS drop | **red**, top of risk board (cancellation short-circuits to 100) |
| **CloudScale Systems** | API usage down 41% + stale escalations | **red**, matches `usage_cliff_core_feature` |
| **Helix Robotics** | champion left + seats down 33% | **red/amber**, matches `champion_departed` |
| **Global Peak Inc** | 94% seat capacity, hard cap in ~8 days | **red** on the *expansion* board, matches `seat_capacity_expansion` |
| **Kestrel Bank** | renewed early + CSAT 9.6 | **green**, matches `early_renewal_advocate` |

Watch how two accounts with a similar *number* can get different plays: the
reranker matches each one to the playbook whose story fits.

## What the numbers in the UI mean

- **Signal score (0-100).** The deterministic roll-up. Risk signals add,
opportunity signals subtract, each weighted by severity; a `usage_drop` is
additionally scaled by its magnitude. An active `stripe_cancellation`
short-circuits to 100 (red).
- **Reranker score (per playbook).** The cross-encoder (`BGE-reranker-base`)
relevance of this account's summary to each shortlisted playbook. The top-3
cosine candidates are reranked; the highest-scoring one (highlighted) drives
the play.
- **ARR at stake.** The account's ARR — what a save or an expansion is worth.

## Model lineup

| Stage | Model | Size | Role |
|---|---|---|---|
| Extract | `urchade/gliner_multi-v2.1` | 280 MB | zero-shot NER on the account summary (display) |
| Encode | `sentence-transformers/all-MiniLM-L6-v2` | 80 MB | 384-dim dense encoder for cosine retrieval |
| Score | `BAAI/bge-reranker-base` | 280 MB | cross-encoder reranker on the top-K playbooks |
| Chat (optional) | e.g. `Qwen/Qwen3-4B-Instruct-2507` | — | drafts the account brief via `/v1/chat/completions` |

The first three live in the `default` CPU bundle and are preloaded by
`compose.yml`. The chat model is opt-in and needs the generation bundle / a GPU.

## SIE features used

- **`extract`** — GLiNER zero-shot NER surfaces typed entities from the account
context.
- **`encode`** — MiniLM turns the account story into a dense vector; the playbook
corpus is pre-encoded once (`npm run index`).
- **`score`** — the cross-encoder reranks the cosine shortlist so the *right*
playbook wins, not just the lexically closest one.
- **`chat/completions`** *(optional)* — OpenAI-compatible endpoint drafts the
brief, grounded in the matched playbook.

One cluster, one round-trip, four model roles — no separate model server per
task.

## What's in the box

```
account-signal-scoring/
├── compose.yml # local SIE server (CPU, 3 models preloaded)
├── data/
│ ├── accounts.json # 8 sample accounts with signals
│ └── playbooks.json # 8 past-outcome playbooks (the corpus)
├── src/
│ ├── config.ts # models, thresholds, paths
│ ├── types.ts # Account, Signal, Playbook, AccountBrief
│ ├── signals.ts # signal catalog + deterministic score roll-up
│ ├── score.ts # extract → encode → cosine → rerank → brief
│ ├── brief.ts # LLM brief (chat/completions) + deterministic fallback
│ ├── events.ts # typed SSE events
│ ├── index-build.ts # one-time playbook-corpus encode
│ └── cli.ts # headless "rank the whole book" scorer
└── web/
├── server.ts # tiny HTTP + SSE server
└── public/ # vanilla-JS UI (no build step)
```

## Extend it

- **Bring your own signals.** Edit `data/accounts.json`, or wire the loader to
your CRM (the original project pulled from Attio + Stripe). The signal catalog
and weights live in `src/signals.ts`.
- **Grow the corpus.** Add rows to `data/playbooks.json` and re-run
`npm run index`. More outcomes → sharper play matching.
- **Swap models.** Change `src/config.ts` — any encoder/reranker in the SIE
catalog works with the same code.

## Honest scope and known limits

- The sample data is synthetic and small; the reranker signal is meaningful but
the corpus is a demo corpus, not a trained ranker.
- The deterministic score is intentionally simple and tunable — it's a starting
point, not a churn model. The point of the demo is the *ranking + play*
matching that SIE adds on top.
- The LLM brief is best-effort: any error falls back to the deterministic writer
so the board never breaks.

## Built with

- Original hackathon project ("Rick") by the Attio-integration team from the
`{Tech: Europe}` London AI Hackathon.
- [SIE](https://github.com/superlinked/sie) — self-hosted inference for agents.
- Apache 2.0.
30 changes: 30 additions & 0 deletions examples/account-signal-scoring/compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
services:
sie:
image: ghcr.io/superlinked/sie-server:latest-cpu-default
platform: linux/amd64
command:
- serve
- --port
- "8080"
# All three models live in the `default` SIE bundle. Combined ~440 MB.
# The optional LLM brief uses a generation model (SIE_CHAT_MODEL) that is
# NOT preloaded here — it needs the generation bundle / a GPU. Without it
# the demo falls back to a deterministic brief, so this stays CPU-only.
- --preload
- urchade/gliner_multi-v2.1,sentence-transformers/all-MiniLM-L6-v2,BAAI/bge-reranker-base
ports:
- "8080:8080"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 5s
timeout: 3s
retries: 60
start_period: 240s
volumes:
- sie-cache:/app/.cache/huggingface

volumes:
sie-cache:
name: sie-cache
external: false
120 changes: 120 additions & 0 deletions examples/account-signal-scoring/data/accounts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
[
{
"id": "pemberton",
"name": "Pemberton & Co",
"domain": "pemberton.co",
"owner": "Rae Whitlock",
"arr": 142000,
"seats": 132,
"seatsUsed": 120,
"renewalDays": 9,
"contact": { "name": "James Pemberton", "title": "VP Operations" },
"signals": [
{ "type": "stripe_cancellation", "note": "Plan downgrade staged in Stripe for next cycle", "detected": "2d ago" },
{ "type": "negative_support_ticket", "note": "Exec NPS dropped from 4 to 1 after a billing incident", "detected": "5d ago" }
]
},
{
"id": "cloudscale",
"name": "CloudScale Systems",
"domain": "cloudscale.io",
"owner": "Marcus Vela",
"arr": 180000,
"seats": 240,
"seatsUsed": 150,
"renewalDays": 14,
"contact": { "name": "Dana Reyes", "title": "Director of Engineering" },
"signals": [
{ "type": "usage_drop", "note": "Core API calls down 41% over 21 days", "detected": "3d ago", "value": 41 },
{ "type": "negative_support_ticket", "note": "Two escalations unresolved past 72h", "detected": "6d ago" }
]
},
{
"id": "helix",
"name": "Helix Robotics",
"domain": "helixrobotics.com",
"owner": "Anika Roth",
"arr": 96000,
"seats": 80,
"seatsUsed": 52,
"renewalDays": 31,
"contact": { "name": "Priya Nair", "title": "Head of Platform" },
"signals": [
{ "type": "usage_drop", "note": "Active seats down 33% since the champion left", "detected": "8d ago", "value": 33 },
{ "type": "negative_support_ticket", "note": "Renewal sponsor departed the company", "detected": "11d ago" }
]
},
{
"id": "atlaspay",
"name": "Atlas Pay",
"domain": "atlaspay.com",
"owner": "Priya Sundar",
"arr": 320000,
"seats": 500,
"seatsUsed": 470,
"renewalDays": 47,
"contact": { "name": "Sofia Marenco", "title": "Chief Operating Officer" },
"signals": [
{ "type": "usage_drop", "note": "Daily actives flat for 14 days, no growth", "detected": "4d ago", "value": 12 }
]
},
{
"id": "kestrel",
"name": "Kestrel Bank",
"domain": "kestrelbank.com",
"owner": "Priya Sundar",
"arr": 525000,
"seats": 600,
"seatsUsed": 540,
"renewalDays": 340,
"contact": { "name": "Margaret Lowe", "title": "Head of Digital" },
"signals": [
{ "type": "renewal_approaching", "note": "Renewed 11 months early", "detected": "20d ago" },
{ "type": "positive_support_ticket", "note": "CSAT 9.6 across the last 10 tickets", "detected": "9d ago" }
]
},
{
"id": "lumen",
"name": "Lumen Health",
"domain": "lumenhealth.io",
"owner": "Rae Whitlock",
"arr": 410000,
"seats": 450,
"seatsUsed": 423,
"renewalDays": 188,
"contact": { "name": "Aaron Vance", "title": "Chief Technology Officer" },
"signals": [
{ "type": "usage_near_limit", "note": "94% seat capacity, up 22% month-over-month", "detected": "1d ago", "value": 94 },
{ "type": "positive_support_ticket", "note": "Champion referred two internal teams", "detected": "12d ago" }
]
},
{
"id": "globalpeak",
"name": "Global Peak Inc",
"domain": "globalpeak.com",
"owner": "Marcus Vela",
"arr": 290000,
"seats": 320,
"seatsUsed": 301,
"renewalDays": 210,
"contact": { "name": "Nina Kovac", "title": "VP Revenue" },
"signals": [
{ "type": "usage_near_limit", "note": "94% seat capacity, hard cap in about 8 days", "detected": "2d ago", "value": 94 },
{ "type": "positive_support_ticket", "note": "NPS 72 this quarter", "detected": "15d ago" }
]
},
{
"id": "sandhill",
"name": "Sandhill Capital",
"domain": "sandhill.fund",
"owner": "Anika Roth",
"arr": 96000,
"seats": 110,
"seatsUsed": 104,
"renewalDays": 96,
"contact": { "name": "Grace Okonkwo", "title": "Partner" },
"signals": [
{ "type": "usage_near_limit", "note": "95% seat capacity with steady growth", "detected": "3d ago", "value": 95 }
]
}
]
Loading