Skip to content

AI pipeline

What happens between "an alert lands" and "a verdict gets written." SocTalk's triage layer is a LangGraph state machine — a supervisor that routes work to specialist worker nodes, then a verdict node that decides whether the case needs human review.

This page is the mental model. The code lives in src/soctalk/graph/, src/soctalk/supervisor/, and src/soctalk/workers/.

Nodes

NodePurposeModel used
supervisorDecides what to do next. Pure routing — does no domain work itself.fast model
wazuh_workerPulls the alert in context, extracts observables (IPs, hashes, users, processes), correlates with recent alerts in the same tenant.fast model
cortex_workerSends observables to Cortex analyzers (VirusTotal, AbuseIPDB, etc.) for reputation/enrichment.fast model
misp_workerLooks up observables against MISP threat-intel feeds for known campaign / actor context.fast model
verdictReasons over everything the workers gathered. Outputs `escalateclose
human_reviewPauses the run; emits a review request to the dashboard queue and/or Slack. Waits on a HumanDecision (`approvereject
closeGenerates the closure report and writes the disposition (`close_fpescalate

Supervisor routing

The supervisor's only job is to pick the next node. Its decision space is a fixed 5-element enum:

DecisionMeans
INVESTIGATEI don't know enough about this alert yet. Run the Wazuh worker.
ENRICHI have observables I haven't reputation-checked. Run Cortex.
CONTEXTUALIZEThe observables look interesting; check for known campaigns/actors. Run MISP.
VERDICTI have enough. Hand to the verdict node.
CLOSEThis is a clear-cut case (e.g., obvious false positive or already-resolved alert). Skip the verdict node.

The supervisor never invokes external tools itself. It reads the accumulated SecOpsState (alerts, observables, prior worker outputs, verdicts) and outputs one of the five decisions. Most cases cycle supervisor → worker → supervisor → worker → supervisor → VERDICT, three to six hops total.

Verdict node

The reasoning model gets the whole accumulated state — original alert, every worker's findings, all observables with their enrichment, prior verdict attempts (if NEEDS_MORE_INFO looped). It outputs:

FieldType
decision`escalate
confidenceenum: `low
rationaleshort markdown
evidence_strength`weak
verdict`benign
impact`low

escalate always goes through human_review. close skips human review and goes straight to close. needs_more_info returns to the supervisor with a prompt suggesting what's still missing.

Human review gate

human_review pauses the run. The case appears in the Review queue on the dashboard and (if Slack is configured) on the Slack two-way HIL. The human picks:

DecisionEffect on the case
approvePending review marked completed + feedback audited. Not auto-resumed; analyst follow-up.
rejectCase closes as auto_closed_fp. Terminal — graph is not re-invoked.
more_infoReview marked info_requested with the questions list. Not auto-resumed; analyst follow-up.

The human's identity, timestamp, and rationale are appended to the case's append-only case_events log.

Run lifecycle

A "run" is one execution of the graph against one case. Status enum:

StatusMeans
activeGraph is executing.
waiting_on_gatePaused at human_review.
pausedManually paused by an MSSP admin.
halted_budgetHit the per-run token budget. Normal V1 runs pick up tokens_budget = 200,000 from the case_runs row (model default). The SOCTALK_CASE_RUN_TOKEN_BUDGET env (default 15,000) is only used as a fallback when the row has no value set.
completedGraph reached close and wrote a disposition.
failedGraph errored or external tool unreachable.

Token budgets are tracked per-run, per-tenant, and install-wide. See Observability for the metrics, LLM providers for the cost knobs.

The runs-worker process

Each tenant has its own runs-worker pod (in the tenant-<slug> namespace) that consumes the queue:

  1. Calls POST /api/internal/worker/runs/claim for a run assigned to its tenant.
  2. Builds the LangGraph from the chart of nodes.
  3. ainvoke() against the graph, posting POST /api/internal/worker/runs/{run_id}/heartbeat every 20 s.
  4. On completion, posts the final state and disposition to POST /api/internal/worker/runs/{run_id}/complete.

The runs-worker is the only per-tenant compute pod — keeping it in the tenant namespace means an over-budget tenant can't starve the rest of the install for compute. The supervisor + worker + verdict logic itself is stateless; the heavy lifting is the LLM calls (out of cluster, billed to the tenant's configured provider).

Source pointers

ConceptFile
Graph builder + routingsrc/soctalk/graph/builder.py
Supervisor logicsrc/soctalk/supervisor/node.py
Verdict nodesrc/soctalk/supervisor/verdict.py
Worker nodessrc/soctalk/workers/
Closure / dispositionsrc/soctalk/graph/close.py
Runs worker loopsrc/soctalk/runs_worker/main.py
State schemasrc/soctalk/models/state.py

Released under the Apache 2.0 License.