-
-
Notifications
You must be signed in to change notification settings - Fork 0
AI Features
All AI surfaces share a common infrastructure:
-
OpenAIClient(source/app/iris_engine/ai/openai_client.py) — thin wrapper around the OpenAI-compatible HTTP API; backends can be LM Studio, Claude proxy, or any OpenAI-compatible endpoint -
case_ai_artifacttable — content-addressed cache keyed oninput_hash(MD5 of prompt + payload); adding one IOC re-runs only the affected specialist, not the whole summary -
Async job queue —
POSTto any AI endpoint returns202 + task_id; a dedicatedai_workercontainer runs jobs off theai_queueso long LLM calls never block a gunicorn worker
/manage/settings → AI tab
Two-slot design — a Primary backend and an Alt backend. The active-slot radio switches all AI calls with no restart.
Per-feature overrides: a collapsible table at the bottom of the AI tab lets you pin individual surfaces to a specific slot regardless of the global radio.
| Backend | How to configure |
|---|---|
| LM Studio (local) | Primary slot URL: http://host.docker.internal:1234/v1
|
| Claude proxy sidecar | Alt slot URL: http://claude_proxy:7440/v1 (Docker internal DNS) |
| OpenAI / Azure OpenAI | Any OpenAI-compatible URL + API key |
docker/claudeProxy/iris_claude_proxy.py — OpenAI-compatible HTTP shim around the
claude-code CLI; listens on port 7440 inside the iris_backend Docker network.
OAuth credentials from ~/.claude/.credentials.json are mounted read-only. When tokens
expire the proxy returns HTTP 401 with code: claude_oauth_expired.
Image rebuild is required for any code change — iris_claude_proxy.py is COPY'd
into the image, not volume-mounted.
For AI features with a "reason then compose" shape, the compose stage routes to a smaller/faster sibling model:
| Configured model | Synthesizer routes to |
|---|---|
| Claude Opus / Sonnet | claude-haiku-4-5 |
| LM Studio / other | unchanged (same model) |
This is configured via SYNTHESIZER_FAST_MODEL_MAP in case_summary.py.
When adding a new "reason then compose" feature, add the pairing there.
Endpoint: POST /api/v2/cases/<id>/ai/summary
Prompt files: case_summary.md (synthesizer) + case_summary_{notes,timeline,iocs,assets}.md
Cache kind: case_summary:<domain> (specialists) + case_summary (final)
Multi-pass map-reduce:
- Four domain specialists (notes / timeline / IOCs / assets) run in a
ThreadPoolExecutor - A synthesizer composes a 7-section executive output from the specialists' output
Cold path: ~12 s on LM Studio gpt-oss-20b, ~26 ms on full cache hit.
Pool worker threads do NOT inherit the calling thread's Flask app context — each worker
pushes its own with app.app_context():. ORM objects created in worker context detach
on exit; return artifact IDs and re-fetch from the main thread.
?sync=true keeps the old inline path for scripted use.
Endpoint: POST /api/v2/cases/<id>/ai/ask
Prompt files: case_chat_<variant>.md (falls back to case_chat.md)
Body: {question, history, variant?, csrf_token}
Available on six case-detail tabs: Notes, Timeline, Assets, IOC, Tasks, Evidence.
Each tab passes chat_variant to the include which loads the matching specialized prompt.
Multi-turn; client owns the history array.
?sync=true bypasses the job queue for scripted use.
Endpoint: POST /api/v2/cases/<id>/ai/timeline/events/<eid>/analysis
Prompt file: event_analysis.md
Cache kind: event_analysis:<event_id>
3 paragraphs: what the event detects / what likely happened / triage hint (80-160 words). Right slide-in opened by clicking a timeline card body.
Endpoint: POST /api/v2/cases/<id>/ai/timeline-analysis
Prompt file: case_timeline_analysis.md (prompt id TimelineNarrativeSystemPrompt-v4)
Cache kind: timeline_analysis
Three sections (prose only, 250-450 words):
- What the timeline tells us
- What's still uncertain
- Where to dig next
Flag-aware: is_flagged: false events contribute with HIGH confidence (reviewed fact);
is_flagged: true events contribute with MEDIUM confidence (provisional).
The panel header shows model · prompt_id · age from the cached artifact.
Endpoint: POST /api/v2/cases/<id>/ai/attack-suggestion
Stateless (not cached)
Returns up to 4 validated ATT&CK technique IDs plus a single UKC v1.3 phase.
Per-type regex shape validation, confidence ≥ 0.5 enforced server-side.
A Set Event Category button on the event modal auto-selects the matching dropdown option.
UKC phases are also wired into the Event Category dropdown (7 phases added via post_init).
Endpoint: POST /api/v2/cases/<id>/ai/ioc-extraction
Stateless (not cached)
Returns up to 10 IOC candidates with:
-
type_idpre-resolved against the liveIocTypetable - Per-type regex shape sanity (drops mis-classified IOCs)
- Noise flags (
⚠ Public DNS resolver,⚠ CDN domain, etc.) - Dedup against existing case IOCs (renders
in caseinstead of+ add)
Accept all is async/await-serialized to avoid celery prefork concurrency crashes.
Available in both the modal note editor and the inline (full-page) note editor.
+ add auto-fires POST /api/v2/cases/<id>/iocs/<id>/source-notes to create the IOC ↔ Note provenance link.
Endpoint: POST /api/v2/cases/<id>/ai/evidence-type-suggestion
Stateless (not cached)
Auto-fires alongside the hash/size step when the analyst clicks Process in the
Register Evidence modal. Reads filename + size + first 4 KB as hex (file never leaves
the browser via FileReader). Returns one validated EvidenceTypes catalog entry with
confidence + reason. Auto-applies to the dropdown; analyst override clears the chip.
Evidence type catalog is snapshotted at request time — admin-added types are picked up automatically.
Endpoint: POST /api/v2/alerts/<alert_id>/ai/case-template-suggestion
Stateless (not cached)
Auto-fires when the Escalate modal opens. Pulls alert title + description + source +
severity + classification + tags + first 20 IOCs + first 20 assets from the Alert ORM.
Returns one validated CaseTemplate with confidence + reason. Validated 13/13 MATCH
against the full template catalog at ≥ 0.92 confidence.
Regression harness: scripts/sim_alert_template_pick.py --all (exit 2 on any non-MATCH).
Endpoint: POST /api/v2/cases/<id>/ai/tag-suggestion (object-type variants)
Stateless (not cached)
Validates output against the bundled MISP catalog (169 taxonomies + 122 galaxies = 66,109 records). ✨ Suggest tags pill appears on IOC, asset, task, and event modals.
Endpoint: POST /api/v2/correlation/cluster-narrative
Cache: case_ai_artifact anchored to min(cluster.case_ids), kind cluster_narrative:<cluster_id>
Generates a short narrative for a cross-case IOC cluster. Cached server-side;
force: true bypasses cache. Also cached client-side in CORR._narrativeCache[cluster_id]
for toggle-within-session without API calls.
POST to /api/v2/cases/<id>/ai/summary or .../ai/ask returns:
{"task_id": "abc123", "state": "queued"}Poll:
GET /api/v2/ai/jobs/<task_id>
→ {state: "running"|"done"|"error"|"cancelled", result: {...}}
DELETE /api/v2/ai/jobs/<task_id> # cancel a queued job
GET /api/v2/ai/jobs?case_id=<id>&state=<state> # list jobs
Adding a new async surface: add one entry to the FEATURES registry in
source/app/iris_engine/ai/ai_jobs.py ({runner, kind: 'artifact'|'dict', priority}).
No dispatcher edits needed.
OpenAIClient.extract_content() strips internal chain-of-thought before returning text
to orchestrators. Callers receive clean JSON/text and need no per-feature handling.
| Format | Models | Stripping |
|---|---|---|
| Gemma-4 channel format | Gemma-4, Gemma-4-e4b | Extracts everything after the last <|channel>output marker; if the output channel is empty, falls back to _last_json_object(thought) which walks the thought channel backwards for the last valid JSON block |
<think> tags |
DeepSeek R1, Qwen-thinking | Strips <think>…</think> with re.DOTALL; also handles truncated (unclosed) tags |
content = None is normalised to "" before stripping.
Reasoning models spend 500–1 000 tokens on the thinking step before emitting output.
Budget default_max_tokens = thinking_budget + output_budget:
- The ATT&CK suggester uses
default_max_tokens = 2 000(was 800 — reasoning models hitfinish_reason=lengthmid-JSON fence at 800) - Short JSON responses still need at least 200–400 tokens above expected output size
- When adding a new orchestrator, test with a small
max_tokensfirst to surface truncation
Diagnostic: "AI backend returned non-JSON content (parse error: Expecting value: line 1 column 1 (char 0))" → model completed its thinking channel but emitted nothing in the output channel; either max_tokens is too low or _last_json_object found no valid JSON in the thought.
Three layers all need matching headroom when adding any synchronous AI path:
| Layer | Setting | Current value |
|---|---|---|
OpenAIClient.timeout |
urllib socket timeout | 600 s |
Claude proxy TIMEOUT_SECONDS
|
subprocess.run timeout |
600 s |
nginx proxy_read_timeout
|
AI endpoint location block | 10 min |
Gunicorn --timeout
|
worker ceiling | 600 s (baked into entrypoint, needs rebuild) |
Fast suggesters (ATT&CK, IOC, evidence type, case template) use 60-90 s.
When the Claude proxy is the active backend and an orchestrator passes
metadata={"iris_case_id": case_id}, the proxy reads/writes a per-case memory
directory at /case_memory/<case_id>/:
-
CLAUDE.md— proxy-readable index -
facts.md— auto-refreshed by iris-ng on case mutations (SQLAlchemy listeners) -
scratchpad.md— LLM-managed working memory (appended via<scratchpad-append>markers the proxy strips from output and persists)
LM Studio silently ignores metadata.iris_case_id — memory is a Claude-only feature.
- Create
source/app/iris_engine/ai/<name>.py - Call
OpenAIClient.build_default_client(feature='<prompt_file_stem>')— this makes the surface participate in per-feature backend routing automatically - Load the prompt from
source/app/resources/ai_prompts/<name>.md - Cache output in
case_ai_artifactwith a descriptivekinddiscriminator - If the orchestrator has a "reason then compose" shape, add the model pairing to
SYNTHESIZER_FAST_MODEL_MAPincase_summary.py - If it should be async, add it to
FEATURESinai_jobs.py - Add the
featurekey to the per-feature override table inmanage_srv_settings.html