Files
redmine/docs/contact_triage_loop_workflow.md
Jason Thistlethwaite 9ee9eb3ec6 Organized
2026-10-17 20:50:35 -04:00

212 lines
7.7 KiB
Markdown

# Contact Triage Loop Workflow (Vision + Operating Pattern)
## Why this exists
This document captures Jason's intended **repeatable contact-review loop** so work can proceed in deterministic batches without reprocessing the same contacts immediately.
Captured from uploaded project note on **2026-07-04** (saved into repo context to avoid losing the design intent).
Raw source capture:
- `/home/jason/projects/redmine/docs/contact_triage_loop_raw_notes_2026-07-04.md`
---
## Core loop pattern
This is a **multi-pass routing system**, not a final one-shot classifier.
1. Select a small batch (10-20 contacts) where `updated_on` is old enough and optional tag constraints match.
2. Sort **newest stale first** (recently stale / last-updated descending), so the loop works through the freshest cooled-down contacts first.
3. For each contact:
- quick contact-only triage
- inspect recent helpdesk tickets/messages when the current pass requires it
- route by adding/removing tags that represent either classifications or workflow state
4. Apply tag updates:
- if the pass identifies a route, add that route/classification tag
- otherwise add a status marker like `ai-checked`
5. Because contact metadata was updated, `updated_on` advances.
6. On future runs, recently processed contacts fall out of the immediate stale batch; later passes can pick them up again when stale or when a designated tag routes them to another worker.
7. Later filters/workers may deliberately remove status tags such as `ai-checked` after consuming them, allowing the contact to re-enter another pass of the conveyor belt.
This forms a self-advancing conveyor belt for incremental cleanup/enrichment. Tags are not all semantic classifications: some are routing/status markers. For example, `ai-checked` means this pass touched the contact; it does not mean the contact's final identity is "AI checked." Likewise, `spam`/`promotions` can be step 1 in a longer handling pipeline, not the final disposition.
Operational objective: reducing clutter in the customer-service/sales channel is valid even when a sender is technically a vendor, billing system, automailer, or promotional sender, because Redmine customer-service is not the only channel available to them.
---
## Existing tooling alignment (current)
`redmine_contacts.py` already supports key building blocks:
- batch candidate selection:
- `batch-update --max-contacts`
- date/tag filters (`--updated-before`, `--with-tag`, `--without-tag`, `--no-tags`)
- deterministic ordering (`--order newest-updated-first` etc)
- safe mutation workflow:
- dry-run by default
- write only with `--apply`
What is still missing for full loop automation:
- richer FILTER-2 workers that classify contacts and mutate tickets/contacts when needed
- scoring engines that map evidence to tags with thresholds
- downstream queue workers for enrichment/NMI/company/person linking
Implemented tracer pieces:
- `redmine_contacts.py loop-run`
- profile contract in `docs/contact_loop_filter_contract.md`
- sample tracer profile: `profiles/contact-loop/noop-ai-checked.json`
- sample no-op FILTER-2 worker: `profiles/contact-loop/filters/noop_filter2.py`
The intended architecture is now: **runner owns conveyor-belt mechanics; FILTER-2 owns domain-specific ticket/contact mutation logic.**
---
## Suggested scoring framework (v0)
### customer
Signals:
- internal/team response exists (`+1`)
- discussion indicates warehouse packages/items (`+1`)
### promo + spam
Signals:
- sender hints (`no-reply`, `noreply`, `deals`, `promotions`) (`+1`)
- message mentions discount/coupon/deal language (`+1`)
Threshold model (initial):
- tag set applied when score >= `2`
- if no tag set crosses threshold: add `ai-checked`
---
## Diagrams
### Diagram 1 — Conveyor-belt loop
```mermaid
flowchart TD
A[Candidate pool\nRedmine contacts] --> B[Select stale contacts\nupdated_before + tag filters\nmax 10-20]
B --> C[Sort newest stale first\nupdated_on descending]
C --> D{For each contact}
D --> E[Run current pass\nFILTER-1 / worker gate]
E -->|pass can route immediately| F[Add route/status/classification tags]
E -->|needs evidence| G[Fetch recent tickets/messages]
G --> H[Manual review or FILTER-2 worker]
H --> I{Route decided?}
I -->|yes| F
I -->|no| J[Add status marker\nai-checked]
F --> K[Update contact/tickets as allowed]
J --> K
K --> L[updated_on advances\ncontact leaves immediate batch]
L --> M[Later passes use staleness + tags\nto route next work]
M --> N[Later worker may remove status tag\ne.g. ai-checked]
N --> B
```
### Diagram 2 — Tag/status re-entry lifecycle
```mermaid
stateDiagram-v2
[*] --> Eligible: updated_on older than cutoff\nand tag filters match
Eligible --> InCurrentPass: newest-stale-first selection
InCurrentPass --> Routed: worker adds route tag\nspam/promotions/etc.
InCurrentPass --> Checked: no route yet\nadd ai-checked
Routed --> CoolingDown: tag update bumps updated_on
Checked --> CoolingDown: status tag update bumps updated_on
CoolingDown --> HeldOut: too fresh for current cutoff
HeldOut --> Eligible: time passes / becomes stale again
Checked --> DownstreamQueued: later worker selects ai-checked\nor another status tag
DownstreamQueued --> ReEntered: worker removes ai-checked\nafter consuming status marker
ReEntered --> Eligible: tag filters match next pass
```
---
## Future queue/tag orchestration pattern
The long-term architecture should treat tags as explicit work queues, for example:
- `needs-summary` -> summary worker processes recent communications, then removes tag
- `needs-enrichment:*` -> enrichment workers (social/entity/profile data)
- `needs-nmi-mapping` -> account-linkage worker for NMI mapping tasks
- `special-handling:*` -> SLA/VIP/high-risk handling workflows
This lets one loop classify and route, while specialized workers execute downstream tasks.
---
## Proposed implementation phases
### Phase 1 (implemented tracer)
- Add `loop-run` command that performs a **per-contact review loop** in dry-run mode:
- select candidates
- run FILTER-1
- fetch recent tickets/messages
- call a profile-defined FILTER-2 command
- print proposed runner-owned tag updates
### Phase 2
- Replace no-op FILTER-2 with a classifier/mutator worker.
- Use FILTER-2 to update contacts/tickets directly through helper CLI calls.
- Keep runner-owned fallback `ai-checked` and RTAG removal semantics.
### Phase 3
- Add queue tags + worker handoff contracts (`needs-summary`, `needs-nmi-mapping`, etc.).
### Phase 4
- Integrate external systems (NMI mapping tables, obsidian profile updates, comm summaries, etc.) with strict scoped guards.
---
## Practical run pattern (current CLI)
Dry-run stale-first loop tracer:
```bash
./redmine_contacts.py \
--base-url http://192.168.50.170 \
--project customer-service \
loop-run \
--profile profiles/contact-loop/noop-ai-checked.json \
--source cache \
--updated-before 2026-06-01 \
--without-tag ai-checked \
--max-contacts 10 \
--order newest-updated-first
```
Apply runner-owned fallback/default tags after reviewing output:
```bash
./redmine_contacts.py \
--base-url http://192.168.50.170 \
--project customer-service \
loop-run \
--profile profiles/contact-loop/noop-ai-checked.json \
--source cache \
--updated-before 2026-06-01 \
--without-tag ai-checked \
--max-contacts 10 \
--order newest-updated-first \
--apply
```
Older dry-run batch-update skeleton:
```bash
./redmine_contacts.py \
--base-url http://192.168.50.170 \
--project customer-service \
batch-update \
--source cache \
--updated-before 2026-06-01 \
--with-tag customer \
--max-contacts 20 \
--order newest-updated-first \
--set background=ai-checked
```
> Replace payload fields/tags only after reviewing dry-run output.