Skip to content

AI Features

zach115th edited this page Jul 16, 2026 · 7 revisions

AI Features

Overview

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_artifact table — content-addressed cache keyed on input_hash (MD5 of prompt + payload); adding one IOC re-runs only the affected specialist, not the whole summary
  • Async job queuePOST to any AI endpoint returns 202 + task_id; a dedicated ai_worker container runs jobs off the ai_queue so long LLM calls never block a gunicorn worker

Backend configuration

Admin UI

/manage/settingsAI 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.

Supported backends

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

Claude proxy sidecar

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 changeiris_claude_proxy.py is COPY'd into the image, not volume-mounted.

Synthesizer model routing

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.

Surfaces

Executive case summary

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:

  1. Four domain specialists (notes / timeline / IOCs / assets) run in a ThreadPoolExecutor
  2. 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.

Case-scoped chat assistant

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.

Per-event AI analysis drawer

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.

Running master-timeline analysis panel

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):

  1. What the timeline tells us
  2. What's still uncertain
  3. 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.

MITRE ATT&CK + Unified Kill Chain suggestion

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).

IOC extraction from notes

Endpoint: POST /api/v2/cases/<id>/ai/ioc-extraction
Stateless (not cached)

Returns up to 10 IOC candidates with:

  • type_id pre-resolved against the live IocType table
  • Per-type regex shape sanity (drops mis-classified IOCs)
  • Noise flags (⚠ Public DNS resolver, ⚠ CDN domain, etc.)
  • Dedup against existing case IOCs (renders in case instead 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.

AI-suggested evidence type

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.

AI-suggested case template on alert escalation

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).

AI tag suggester

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.

AI cluster narrative

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.

Async job queue

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.

Reasoning model support

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.

max_tokens for reasoning models

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 hit finish_reason=length mid-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_tokens first 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.

Timeout chain

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.

Per-case Claude memory

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.

Adding a new AI orchestrator

  1. Create source/app/iris_engine/ai/<name>.py
  2. Call OpenAIClient.build_default_client(feature='<prompt_file_stem>') — this makes the surface participate in per-feature backend routing automatically
  3. Load the prompt from source/app/resources/ai_prompts/<name>.md
  4. Cache output in case_ai_artifact with a descriptive kind discriminator
  5. If the orchestrator has a "reason then compose" shape, add the model pairing to SYNTHESIZER_FAST_MODEL_MAP in case_summary.py
  6. If it should be async, add it to FEATURES in ai_jobs.py
  7. Add the feature key to the per-feature override table in manage_srv_settings.html

Clone this wiki locally