Skip to content

Commit 2340d15

Browse files
authored
Merge pull request #65 from intelowlproject/gsoc-2026/chatbot-docs
[GSoC 2026] docs(chatbot): user, deployment, developer & fine-tuning guides
2 parents 9fd5d33 + 3bae273 commit 2340d15

10 files changed

Lines changed: 378 additions & 0 deletions

File tree

docs/IntelOwl/advanced_configuration.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,30 @@ FLOWER_PWD
280280

281281
or change the `.htpasswd` file that is created in the `docker` directory in the `intelowl_flower` container.
282282

283+
## Chatbot
284+
285+
_Available from version >= 6.7.0_
286+
287+
The optional LLM chatbot (enabled with the `--ollama` flag, see
288+
[installation](./installation.md#chatbot-ollama)) is configured through the following variables, set
289+
like every other secret in `docker/env_file_app`. All have sensible defaults; override them only if
290+
needed.
291+
292+
| Variable | Default | Purpose |
293+
|---|---|---|
294+
| `OLLAMA_BASE_URL` | `http://ollama:11434` | URL of the Ollama runtime. |
295+
| `OLLAMA_MODEL` | `qwen2.5:3b` | Model the agent uses; must support Ollama tool calling (see [Fine-tuning & Prompting](./chatbot_tuning.md)). |
296+
| `CHATBOT_MESSAGE_RETENTION_DAYS` | `90` | Conversations idle for this many days are pruned by a daily task. |
297+
| `CHATBOT_RATE_LIMIT` | `5` | Max messages a user may send per window (REST and WebSocket share the bucket). |
298+
| `CHATBOT_RATE_LIMIT_WINDOW` | `60` | Rate-limit window, in seconds. |
299+
| `CHATBOT_PENDING_ACTION_TTL` | `600` | Lifetime, in seconds, of a previewed-analysis confirmation before it expires. |
300+
301+
**CPU / GPU.** The chatbot runs on CPU by default and the `qwen2.5:3b` default is sized for that. GPU
302+
passthrough is not yet supported (tracked in
303+
[issue #3717](https://github.com/intelowlproject/IntelOwl/issues/3717)). For changing the model, the
304+
context window, or packaging a custom model, see the
305+
[Fine-tuning & Prompting](./chatbot_tuning.md) guide.
306+
283307
## Manual Usage
284308

285309
The `./start` script essentially acts as a wrapper over Docker Compose, performing additional checks.

docs/IntelOwl/chatbot.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Chatbot
2+
3+
_Available from version >= 6.7.0_
4+
5+
The IntelOwl chatbot is a locally-hosted LLM assistant that answers natural-language questions over
6+
your threat-intelligence data. It runs entirely on your own deployment (Ollama) and **never sends
7+
data to external APIs** — your jobs, observables and reports never leave the instance.
8+
9+
The chatbot is an optional component. If your deployment was not started with the Ollama service,
10+
the chat button does not connect; see the
11+
[deployment guide](./installation.md#chatbot-ollama) to enable it.
12+
13+
## What it can do
14+
15+
Ask in plain language and the assistant answers by calling read-only IntelOwl tools on your behalf:
16+
17+
- search your jobs and show a job's details;
18+
- summarize a job or an investigation;
19+
- show an investigation's job tree;
20+
- show a job's aggregated data model;
21+
- list the analyzers available on the instance;
22+
- recommend a playbook for an observable.
23+
24+
Everything it returns is **scoped to what you can already see** in the UI (your own data, plus what
25+
your organization and TLP visibility allow). The chatbot cannot reveal anything you could not reach
26+
through the normal interface.
27+
28+
## Opening the chat
29+
30+
Click the chat-bubble icon in the top navigation bar to open the chat drawer. The drawer overlays
31+
the current page and the rest of the app stays usable while it is open; it also stays available as
32+
you navigate between pages.
33+
34+
![chat drawer](./static/chatbot/drawer.png)
35+
36+
A small status badge shows whether the assistant is connected and ready.
37+
38+
## Asking a question
39+
40+
Type a question and press send. The answer streams in token by token. When a question needs data,
41+
the assistant calls one of the tools above and shows the result formatted (tables, lists and links
42+
are rendered).
43+
44+
Example questions:
45+
46+
- "Show me my most recent jobs."
47+
- "Summarize job #1234."
48+
- "Which analyzers can run on a domain?"
49+
- "Which playbook should I use for an IP address?"
50+
51+
![a chat turn](./static/chatbot/turn.png)
52+
53+
## Quick actions
54+
55+
Below the input you'll find one-click quick actions. They are **context-aware**: on a job page they
56+
offer job actions ("Summarize this job", "Which plugins ran?", "Show job details", "Evaluate
57+
results"), on an investigation page they offer investigation actions, and elsewhere they offer
58+
general ones ("Show my recent jobs", "List my investigations"). Clicking a chip sends the
59+
corresponding question for you.
60+
61+
![quick actions](./static/chatbot/quick_actions.png)
62+
63+
## Working with the page you're on
64+
65+
The chatbot knows which page you are viewing. If you are on a job or investigation page, you can
66+
refer to "this job" / "this investigation" and the assistant resolves it from the current page —
67+
no need to copy the ID.
68+
69+
## Conversations
70+
71+
Each chat is saved as a conversation so you can come back to it. From the drawer you can:
72+
73+
- start a **new chat**;
74+
- open the **conversation list** to switch between past conversations;
75+
- review a conversation's **history** (older messages load automatically);
76+
- **delete** a conversation you no longer need.
77+
78+
Old conversations are pruned automatically after a retention period configured by the operator (see
79+
the [advanced configuration](./advanced_configuration.md#chatbot)).
80+
81+
## Launching an analysis safely
82+
83+
The chatbot can suggest running a new analysis on an observable, but it **cannot start one by
84+
itself**. When you ask it to analyze something, it shows a **preview** of what would run and a
85+
Confirm / Cancel card. The analysis starts only when **you** click **Confirm** (Cancel discards it).
86+
The same TLP and visibility rules as the normal analysis flow apply.
87+
88+
![confirm an analysis](./static/chatbot/confirm.png)
89+
90+
This is a deliberate safety guardrail: even if the model misbehaves, it has no path to launch an
91+
analysis — and therefore cannot send an observable to external analyzers — without an explicit click
92+
from you.
93+
94+
## Limits and availability
95+
96+
- **Rate limit.** To protect the instance there is a per-user limit on how many messages you can
97+
send per minute; if you hit it, wait a moment and try again.
98+
- **Model availability.** If the chatbot worker or the Ollama service is not running, the drawer
99+
shows an "unavailable" state instead of the connected badge and turns are not served — ask your
100+
operator to enable/restart the Ollama service.
101+
- **Long conversations.** A conversation is kept in full, but a very long one can exceed the model's
102+
context window (`num_ctx`); when that happens Ollama drops the oldest tokens, so the assistant may
103+
lose the earliest messages. There is no automatic summarization of the conversation.

docs/IntelOwl/chatbot_tuning.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Chatbot: Fine-tuning & Prompting
2+
3+
_Available from version >= 6.7.0_
4+
5+
This guide covers choosing and customizing the language model behind the chatbot, tuning the prompt,
6+
and packaging a custom model. It is aimed at operators who want to change the default model or
7+
improve answer quality. For the environment variables referenced here see the
8+
[chatbot configuration](./advanced_configuration.md#chatbot).
9+
10+
## How the model is wired
11+
12+
The chatbot uses [Ollama](https://ollama.com/) as a local LLM runtime and LangChain's native
13+
**tool-calling** agent. At build time the backend creates a `ChatOllama` client
14+
(`api_app/chatbot_manager/agent/agent.py`) pointed at `OLLAMA_BASE_URL` (default
15+
`http://ollama:11434`) with `temperature=0` and a fixed context window, binds the chatbot tools to
16+
it through the tool-calling API, and runs a tool-call → observation loop until the model replies with
17+
plain text.
18+
19+
Two consequences matter for tuning:
20+
21+
- The model **must support Ollama tool calling**. A model that cannot emit tool calls will not work.
22+
- The backend sets the prompt and the key inference parameters itself (see below), so they are the
23+
levers you tune — not a model's baked-in defaults.
24+
25+
## Choosing a model
26+
27+
The default is **`qwen2.5:3b`**. It is chosen on purpose: it is the smallest model that reliably
28+
picks the right tool and answers from the tool output with **usable latency on a CPU-only deploy**
29+
(for comparison, a 7B model such as `mistral` was markedly slower on CPU — minutes per agent round,
30+
often hitting the turn timeout). On stronger
31+
hardware you can switch to any larger tool-capable Ollama model for better answer quality.
32+
33+
Requirements for a replacement model:
34+
35+
- it supports tool calling in Ollama;
36+
- the Ollama server is recent enough to **stream while tools are bound** — IntelOwl pins the Ollama
37+
image to `ollama/ollama:0.30.7` for this reason (versions older than 0.8.0 cannot stream with
38+
tools); keep this in mind if you run Ollama yourself.
39+
40+
## Pointing to a different model
41+
42+
Set the `OLLAMA_MODEL` secret to the model tag you want (it is pulled automatically on first start).
43+
Keep the **three** places that reference the default in sync if you change the baked-in default
44+
rather than just overriding the secret:
45+
46+
- `intel_owl/settings/chatbot.py``OLLAMA_MODEL` default;
47+
- `docker/env_file_app_template``OLLAMA_MODEL`;
48+
- `docker/entrypoints/ollama.sh``DEFAULT_MODEL` (the entrypoint that pulls the model).
49+
50+
For a normal deployment you only set the `OLLAMA_MODEL` secret; the entrypoint pulls it on startup.
51+
52+
## The context window (`num_ctx`)
53+
54+
The backend requests an **8192-token** context window (`_NUM_CTX` in `agent.py`). This is
55+
deliberate: Ollama's default of 2048 tokens silently truncates the prompt (the system prompt plus
56+
the tool schemas already approach ~2.2k tokens), which drops tool definitions and wrecks tool
57+
selection. 8192 fits the prompt, the conversation history and the tool observations comfortably and
58+
keeps the prompt prefix stable across iterations (so follow-up rounds hit Ollama's KV cache).
59+
60+
If you move to a larger model with a bigger context window and longer conversations, raising
61+
`_NUM_CTX` is the knob — at the cost of more memory and slower evaluation.
62+
63+
## The system prompt
64+
65+
The assistant's instructions live in plain text at
66+
`api_app/chatbot_manager/agent/system_prompt.txt`. This — not a model's built-in system message — is
67+
what shapes the assistant's behavior, because the backend sends it as the agent's system prompt on
68+
every turn. The file is organized in sections:
69+
70+
- `[Role]` — who the assistant is and its answer style (concise, data-driven, cite the tools used);
71+
- `[Tools — when to use each]` — one line per tool telling the model when to call it;
72+
- `[Rules]` — hard constraints (only the current user's data; call the right tool instead of
73+
guessing; `analyze_observable` only previews and never claims it launched anything);
74+
- `[Response style]` — formatting expectations.
75+
76+
To tune behavior, edit this file. Practical prompting tips for small local models:
77+
78+
- Keep tool descriptions short and action-oriented ("Use for …"); they compete for context space.
79+
- State hard guarantees in `[Rules]` (data scoping, the preview-only analysis guardrail) — small
80+
models follow short imperative rules better than long prose.
81+
- When you add a new tool, add a matching one-line entry under `[Tools — when to use each]` so the
82+
model knows when to reach for it (see [adding a chatbot tool](./contribute.md)).
83+
84+
## Building a custom model with a Modelfile
85+
86+
Use an Ollama [Modelfile](https://docs.ollama.com/modelfile) to **package the weights** you want to
87+
run — for example a specific quantization, or a fine-tuned model imported from a local GGUF file:
88+
89+
```dockerfile
90+
# Modelfile
91+
FROM qwen2.5:3b-instruct-q4_K_M
92+
# or import your own weights:
93+
# FROM ./my-finetuned-model.gguf
94+
```
95+
96+
Build and register it, then point the chatbot at it:
97+
98+
```bash
99+
ollama create intelowl-llm -f Modelfile
100+
# then set the secret:
101+
OLLAMA_MODEL=intelowl-llm
102+
```
103+
104+
Important: the backend sets `num_ctx`, `temperature` and the system prompt explicitly on every call,
105+
so a Modelfile's `SYSTEM` and its `PARAMETER num_ctx` / `PARAMETER temperature` are overridden for the
106+
chatbot (other `PARAMETER` directives the backend does not set still apply). Use the Modelfile to
107+
choose *which weights* run; use `system_prompt.txt` (and `_NUM_CTX` in
108+
`agent.py`) to change *how the assistant behaves*.
109+
110+
## Validating a model before rollout
111+
112+
After switching or building a model, confirm it actually tool-calls before relying on it:
113+
114+
1. Bring up the stack with the Ollama service and wait for the model to finish pulling.
115+
2. Open the chat and send a question that must use a tool, e.g. **"Show my recent jobs"** or
116+
**"Summarize job #<id>"**.
117+
3. Verify the assistant calls a tool (a tool/status indicator appears) and answers from real data,
118+
rather than replying generically. The same check works through the REST endpoint
119+
`POST /api/chatbot/sessions/message`.
120+
121+
If the model answers without ever calling a tool, it is not tool-calling reliably — pick a different
122+
model or a less aggressively quantized variant.
123+
124+
## Out of scope
125+
126+
Actual model training (LoRA/PEFT fine-tuning, dataset preparation, GGUF conversion of trained
127+
adapters) is outside the scope of this guide. This page covers selecting, configuring, prompting and
128+
packaging existing tool-capable models.

docs/IntelOwl/contribute.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,101 @@ We are setting the field `evaluation` depending on some logic that we constructe
576576
If the IP address has been reported by some AbuseIPDB users but, at the same time, is whitelisted by AbuseIPDB, then we set its `evaluation` to `trusted`. On the contrary, if it's not whitelisted, we set it as `malicious`.
577577

578578

579+
## How to add a chatbot tool
580+
581+
The optional [chatbot](./chatbot.md) is a LangChain tool-calling agent. Its capabilities are plain
582+
Python "tools": each wraps an IntelOwl query and is exposed to the model. Adding a capability means
583+
adding a tool. The agent lives in `api_app/chatbot_manager/agent/`; the tools live in
584+
`api_app/chatbot_manager/agent/tools/`**one file per tool**.
585+
586+
### 1. Write the tool
587+
588+
Create `api_app/chatbot_manager/agent/tools/<your_tool>.py`. A tool is a factory that **closes over
589+
the requesting `user`** and returns a LangChain `@tool`-decorated function. Closing over the user is
590+
what enforces multi-tenancy: every queryset is scoped to that user, and the model can never widen it.
591+
592+
```python
593+
from langchain_core.tools import tool
594+
595+
from api_app.chatbot_manager.agent.tools._common import clamp_limit
596+
from api_app.chatbot_manager.serializers.my_tool import MyToolResultSerializer
597+
598+
599+
def make_my_tool(user):
600+
@tool("my_tool")
601+
def my_tool(query: str = "", limit: int = 10) -> str:
602+
"""One-line description the model reads to decide when to call this tool.
603+
604+
Args:
605+
query: what to search for.
606+
limit: maximum number of results (default 10, max 50).
607+
"""
608+
from api_app.models import Job # heavy/circular imports stay function-local
609+
610+
errors = []
611+
limit = clamp_limit(limit, errors)
612+
# Scope to the user: visible_for_user matches the REST viewsets / UI.
613+
qs = Job.objects.visible_for_user(user).filter(analyzable__name__icontains=query)[:limit]
614+
return MyToolResultSerializer({"errors": errors, "results": qs}).to_json()
615+
616+
return my_tool
617+
```
618+
619+
Conventions to follow (the maintainers enforce them):
620+
621+
- **Scope every query to `user`** with `visible_for_user(user)` (or the appropriate owner/org
622+
filter). Treat all arguments as **untrusted** — they come from the LLM: validate them against the
623+
enums in `api_app/choices.py` and clamp limits with `clamp_limit` (`agent/tools/_common.py`).
624+
- **Return a JSON string** with the same `{"errors": [...], "<payload>": ...}` envelope via a DRF
625+
serializer's `.to_json()` — never hand-build a dict. LangChain feeds the returned string back to
626+
the model as the tool observation.
627+
- Use named constants and top-level imports (keep only heavy/circular imports function-local, as the
628+
existing tools do).
629+
630+
### 2. Add the result serializer
631+
632+
Add `api_app/chatbot_manager/serializers/<your_tool>.py` producing that envelope (build on the
633+
shared base in `serializers/base.py`, like the other tools). One serializer module per tool keeps
634+
parallel PRs from colliding on a shared file.
635+
636+
### 3. Register the tool
637+
638+
Add it to `build_tools()` in `api_app/chatbot_manager/agent/tools/__init__.py`:
639+
640+
```python
641+
from .my_tool import make_my_tool
642+
643+
644+
def build_tools(user) -> list:
645+
return [
646+
# ... existing tools ...
647+
make_my_tool(user),
648+
]
649+
```
650+
651+
### 4. Tell the model when to use it
652+
653+
Add a one-line entry under `[Tools — when to use each]` in
654+
`api_app/chatbot_manager/agent/system_prompt.txt`. The agent binds the tools through
655+
`create_tool_calling_agent`; that line is how the model learns when to reach for yours. See the
656+
[Fine-tuning & Prompting](./chatbot_tuning.md) guide for the prompt structure.
657+
658+
### 5. Test it
659+
660+
Add a per-tool test under `tests/api_app/chatbot_manager/tools/test_<your_tool>.py`. **Mock Ollama
661+
and any HTTP** — tests must never hit a real model or network. Cover the scoping (a second user must
662+
not see the first user's data) and the error/empty branches. For a tool that reads the database, also
663+
keep its **query count invariant to result size** with a query-count guard (an `assertNumQueries` /
664+
`CaptureQueriesContext` test that stays constant as the result set grows), so a future un-prefetched
665+
relation cannot introduce an N+1.
666+
667+
Run the chatbot tests (rebuild the test image first if dependencies changed):
668+
669+
```bash
670+
./start test build && ./start test up
671+
docker exec intelowl_uwsgi python manage.py test tests.api_app.chatbot_manager --keepdb
672+
```
673+
579674
## How to modify a plugin
580675

581676
If the changes that you have to make should stay local, you can just change the configuration inside the `Django admin` page.

docs/IntelOwl/installation.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,32 @@ docker compose --project-directory docker -f docker/default.yml -f docker/postgr
269269
```
270270
</div>
271271

272+
### Chatbot (Ollama)
273+
274+
IntelOwl ships an optional, locally-hosted LLM chatbot (see the [Chatbot](./chatbot.md) user guide).
275+
It is disabled by default and enabled with the `--ollama` flag, which adds the
276+
`docker/ollama.override.yml` compose file. That file starts two extra containers:
277+
278+
- **`ollama`** — the local LLM runtime (image `ollama/ollama:0.30.7`), reachable in-cluster at
279+
`http://ollama:11434`; no data ever leaves the deployment.
280+
- **`celery_worker_chatbot`** — a dedicated Celery worker for the chatbot queue, so chatbot tasks
281+
stay isolated from the main analyzer/connector workers.
282+
283+
```bash
284+
./start prod up --ollama
285+
```
286+
287+
On first start the Ollama entrypoint **pulls the configured model** (`OLLAMA_MODEL`, default
288+
`qwen2.5:3b`); the first pull downloads a few GB and can take several minutes — the chatbot reports
289+
itself unavailable until it completes.
290+
291+
**Hardware.** The default `qwen2.5:3b` is chosen to run on **CPU** with usable latency, so no GPU is
292+
required. Ensure the host has enough free RAM for the model (a few GB for the 3B default; more for
293+
larger models). **GPU passthrough is not yet supported** out of the box (tracked in
294+
[issue #3717](https://github.com/intelowlproject/IntelOwl/issues/3717)). For model selection,
295+
context window and packaging see the [Fine-tuning & Prompting](./chatbot_tuning.md) guide; for the
296+
chatbot environment variables see the [advanced configuration](./advanced_configuration.md#chatbot).
297+
272298
### Stop
273299

274300
To stop the application you have to:
51.6 KB
Loading
16.3 KB
Loading
19.6 KB
Loading
55.8 KB
Loading

0 commit comments

Comments
 (0)