Skip to content

Commit 619b164

Browse files
author
Bob
committed
Add AI helpdesk ticketing, setup assistant & client/site AI history
- Agnostic, settings-defined helpdesk integration: natural-language policy prompt + admin 'helpdesk.js' code box (ai_helpdesk_prompt / api / code) run on the bridge; the AI calls named operations (create_ticket, reply, note, submit_report, ...). Works with any helpdesk/PSA, no vendor code shipped. - 'Use AI to Help Create These' assistant endpoint (HelpdeskAssist) that interviews the admin and drafts both the policy and the integration code. - Auto-ticketing: correct customer (closest-match + safe fallback), dedup + escalate, reopen closed tickets, one combined end-of-batch report + individual failure tickets, device deep-link in tickets, customer-safe replies. - created_by/modified_by stamping on AI tasks & bulk commands. - Client/Site 'AI History' review across all machines (ai/history-scope). - Migrations 0064-0067.
1 parent 5feef29 commit 619b164

11 files changed

Lines changed: 477 additions & 9 deletions

api/tacticalrmm/MARKETING.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<!--
2+
Community-facing overview of the AI Assistant / AI Ticketing feature set.
3+
Intentionally vendor-neutral: no company, customer, or deployment specifics.
4+
Drop screen recordings into docs/media/ and they will animate inline on GitHub.
5+
-->
6+
7+
# 🤖 Pi AI for Tactical RMM — your RMM just grew a night shift
8+
9+
> **What if your RMM didn't just *tell* you something broke — but investigated it, fixed what it safely could, opened a properly-attributed ticket in your helpdesk, updated the customer, and handed your techs a clean, deduplicated queue every morning?**
10+
11+
That's this. It turns Tactical RMM from a monitoring dashboard into an **autonomous, self-documenting AI teammate** that works across your entire fleet — 24/7, unattended, and under your control.
12+
13+
---
14+
15+
## 🎬 See it in action
16+
17+
<!-- Record 10–20s screen captures and drop them here; GitHub auto-plays .gif -->
18+
| | |
19+
|---|---|
20+
| **Right-click a device → talk to it** | ![device chat](docs/media/device-chat.gif) |
21+
| **Scheduled fleet checks that only ticket real problems** | ![scheduled checks](docs/media/scheduled.gif) |
22+
| **Auto-drafted helpdesk integration (two boxes, one AI)** | ![setup assistant](docs/media/assistant.gif) |
23+
| **Client/Site "AI History" review across every machine** | ![ai history](docs/media/history.gif) |
24+
25+
*(No GIFs yet? The written walkthroughs below stand on their own.)*
26+
27+
---
28+
29+
## 🧠 The problem every MSP/IT team lives with
30+
31+
- Alerts fire at 2AM. A human still has to log in, diagnose, decide, and write it up.
32+
- The same issue generates **five duplicate tickets** by morning.
33+
- Tickets land on the **wrong contact** (or a `noreply@` address), so nobody can route them.
34+
- "All good" checks generate noise; real failures get buried.
35+
- Every helpdesk/PSA has a different API, so "just integrate it" is a project.
36+
37+
**Monitoring finds problems. This closes the loop from _detection → diagnosis → ticket → customer comms → clean queue_.**
38+
39+
---
40+
41+
## ⚡ What it actually does
42+
43+
### 1. An AI that *operates* devices — not just chats
44+
Right-click any device → **Pi.dev** → a chat scoped **only** to that machine. It has real, gated shell access through the agent:
45+
- Reads logs, checks services, inspects disks/processes, runs diagnostics.
46+
- Proposes fixes and — with **approve/deny** on every action — runs them.
47+
- Read-only mode, per-role write permissions, and a global **kill switch** for spend.
48+
49+
### 2. Unattended fleet checks that respect your attention
50+
Schedule an AI task ("check this UniFi controller nightly") or fan one prompt across **hundreds of machines** ("verify every backup ran"). Each run investigates and returns a verdict — **and only bothers a human when something is actually wrong.**
51+
52+
Real-world checks teams run today:
53+
- 🖧 **Network/UniFi health** — orphaned devices, bad uplinks, WAN outages.
54+
- 💾 **Backup verification** — every job on every server actually succeeded and is recent.
55+
- 🗄️ **Storage/ZFS health** — degraded pools, failing disks, capacity.
56+
57+
### 3. Tickets done *right*, automatically
58+
When a check finds something actionable, it opens a ticket in **your** helpdesk with the stuff humans always get wrong:
59+
-**Correct customer** — resolved by closest-match, with a safe internal fallback that flags "unmatched" instead of guessing.
60+
-**No duplicates** — a recurring issue **updates and escalates** the existing ticket ("still failing after N checks — needs action now") instead of spawning new ones.
61+
-**Auto-reopen** — if a closed ticket's problem comes back, it reopens with an explanation.
62+
-**One combined report** for everything that's healthy (a single "all-green" summary), and **individual tickets** only for real failures.
63+
-**A deep link to the device** right in the ticket — a logged-in tech clicks straight to it.
64+
-**Customer-safe replies** — the AI writes as a tech, and is hard-blocked from promising schedules, dispatch times, or inventing facts.
65+
66+
### 4. Works with *any* helpdesk — defined in settings, not code
67+
Two boxes in Global Settings:
68+
- 📝 **Policy** (plain English): *when* to open/reply/note, and *which* operations to call.
69+
- 🧩 **Integration code** (`helpdesk.js`): the *precise* API logic for your system.
70+
71+
Swap from one PSA/helpdesk to another by editing text — **no redeploy, no fork, no code shipped for your specific vendor.** The API key stays server-side and is never exposed to the model.
72+
73+
### 5. 🪄 "Use AI to Help Create These"
74+
Don't want to write that integration? Click one button. The AI **interviews you** about your helpdesk (product, API style, auth, how you create/search/reply to tickets), then **drafts both boxes for you** to review and apply. Setup goes from "developer project" to "five-minute conversation."
75+
76+
### 6. Client/Site "AI History" — a manager's dream review
77+
Right-click a **client or site****AI History** → one screen of **everything** the AI did across **every machine** under it: chats, scheduled runs, bulk runs — each tagged with the device, who triggered it, and the outcome. Perfect for QBRs and shift handoffs.
78+
79+
---
80+
81+
## 🌙 A night in the life
82+
83+
```mermaid
84+
flowchart LR
85+
A["🕒 2:00 AM<br/>Scheduled fleet checks fire"] --> B{"Investigate<br/>each device"}
86+
B -->|"all good"| C["📄 ONE combined<br/>'all healthy' report"]
87+
B -->|"failure"| D["🎫 Individual ticket<br/>to the right customer"]
88+
D --> E["🔁 Recurring? update &amp; escalate<br/>Closed? reopen"]
89+
D --> F["🔗 Device deep link<br/>in the ticket"]
90+
C --> G["☀️ 8:00 AM<br/>Techs open a clean,<br/>deduplicated queue"]
91+
E --> G
92+
F --> G
93+
```
94+
95+
Your humans wake up to a **triaged, attributed, no-duplicate queue** — and a single green report proving the rest of the fleet was checked.
96+
97+
---
98+
99+
## 🔒 Built for control (because it has real power)
100+
101+
- **Approve/deny** on every device-changing action; read-only sessions by default for "resolve" flows.
102+
- **Per-role permissions** — who can use AI, which **models** they may pick, who can let it make changes.
103+
- **Model-agnostic** — bring your own provider/key (Anthropic, OpenAI, Google, xAI, OpenRouter, or self-hosted/OpenAI-compatible). Mark a default, expose a curated catalog.
104+
- **Cost guardrails** — session limits, a fleet-wide **emergency stop**, and unattended runs that stay read-only unless you allow otherwise.
105+
- **Least privilege** — the helpdesk service account and API keys live server-side; the model never sees them, and its outputs are scrubbed.
106+
107+
---
108+
109+
## 🏆 Why this is *the* feature for an RMM
110+
111+
An RMM exists to **reduce toil and shrink MTTR**. Everything else is plumbing to that end. This feature attacks it directly:
112+
113+
- **Detection → resolution, not detection → another alert.** The loop actually closes.
114+
- **Your queue becomes signal.** One green report + real tickets only, correctly attributed, no dupes.
115+
- **Institutional knowledge, encoded.** Your triage runbooks become prompts any tech (or the AI) runs consistently at 3AM.
116+
- **Vendor-neutral by design.** It bends to *your* helpdesk, not the other way around.
117+
- **It scales the thing you can't hire fast enough:** senior triage judgment, applied to the whole fleet, every night.
118+
119+
**Monitoring watches. This works.**
120+
121+
---
122+
123+
## 🚀 Getting started
124+
125+
1. Add an AI provider + model in **Global Settings → Pi.dev AI** and mark a default.
126+
2. Grant roles the AI permissions they need.
127+
3. Click **"Use AI to Help Create These"** and let it draft your helpdesk integration.
128+
4. Right-click a device → **Pi.dev**, or schedule your first fleet check.
129+
5. Watch your morning queue get quieter.
130+
131+
---
132+
133+
*Built as an extension to [Tactical RMM](https://github.com/amidaware/tacticalrmm). Bring your own AI provider and helpdesk — everything else is configuration.*

api/tacticalrmm/agents/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,7 @@ def _pi_device_facts(agent):
16801680
"last_logged_in_user": agent.last_logged_in_user,
16811681
"description": agent.description,
16821682
"agent_version": agent.version,
1683+
"device_url": (f"{settings.CORS_ORIGIN_WHITELIST[0]}/agents/{agent.agent_id}" if getattr(settings, "CORS_ORIGIN_WHITELIST", None) else ""),
16831684
"monitoring_type": agent.monitoring_type,
16841685
"last_seen": str(agent.last_seen) if agent.last_seen else None,
16851686
}
@@ -1801,6 +1802,12 @@ def model_dict_full(m):
18011802
),
18021803
"persist_history": bool(core.ai_persist_history),
18031804
"resume_session": request.data.get("resume_session") or None,
1805+
"helpdesk_prompt": core.ai_helpdesk_prompt or "",
1806+
"helpdesk_api": {
1807+
"base_url": core.ai_helpdesk_api_base_url or "",
1808+
"api_key": core.ai_helpdesk_api_key or "",
1809+
},
1810+
"helpdesk_code": core.ai_helpdesk_code or "",
18041811
}
18051812

18061813
token = create_pi_session(data=blob)
@@ -1932,6 +1939,7 @@ def model_dict_full(m):
19321939
"last_logged_in_user": agent.last_logged_in_user,
19331940
"description": agent.description,
19341941
"agent_version": agent.version,
1942+
"device_url": (f"{settings.CORS_ORIGIN_WHITELIST[0]}/agents/{agent.agent_id}" if getattr(settings, "CORS_ORIGIN_WHITELIST", None) else ""),
19351943
"monitoring_type": agent.monitoring_type,
19361944
"last_seen": str(agent.last_seen) if agent.last_seen else None,
19371945
}
@@ -1952,6 +1960,12 @@ def model_dict_full(m):
19521960
"allow_mutating": bool(is_super or (user.role and user.role.can_use_ai_mutate)),
19531961
"persist_history": bool(core.ai_persist_history),
19541962
"resume_session": request.data.get("resume_session") or None,
1963+
"helpdesk_prompt": core.ai_helpdesk_prompt or "",
1964+
"helpdesk_api": {
1965+
"base_url": core.ai_helpdesk_api_base_url or "",
1966+
"api_key": core.ai_helpdesk_api_key or "",
1967+
},
1968+
"helpdesk_code": core.ai_helpdesk_code or "",
19551969
}
19561970

19571971
token = create_pi_session(data=blob)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.30 on 2026-07-12 17:13
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0063_bulkaicommand_exclude_agent_ids'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='coresettings',
15+
name='ai_helpdesk_prompt',
16+
field=models.TextField(blank=True, default=''),
17+
),
18+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.30 on 2026-07-12 17:23
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0064_coresettings_ai_helpdesk_prompt'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='coresettings',
15+
name='ai_helpdesk_api_base_url',
16+
field=models.CharField(blank=True, default='', max_length=255),
17+
),
18+
migrations.AddField(
19+
model_name='coresettings',
20+
name='ai_helpdesk_api_key',
21+
field=models.CharField(blank=True, default='', max_length=255),
22+
),
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.30 on 2026-07-12 18:07
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0065_coresettings_ai_helpdesk_api_base_url_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='aitaskrun',
15+
name='batch_id',
16+
field=models.CharField(blank=True, db_index=True, max_length=64, null=True),
17+
),
18+
migrations.AddField(
19+
model_name='bulkaicommand',
20+
name='report_prompt',
21+
field=models.TextField(blank=True, default=''),
22+
),
23+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.30 on 2026-07-12 18:48
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0066_aitaskrun_batch_id_bulkaicommand_report_prompt'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='coresettings',
15+
name='ai_helpdesk_code',
16+
field=models.TextField(blank=True, default=''),
17+
),
18+
]

api/tacticalrmm/core/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@ class CoreSettings(BaseAuditModel):
121121
ai_module_enabled = models.BooleanField(default=False)
122122
ai_persist_history = models.BooleanField(default=True)
123123
ai_require_approval = models.BooleanField(default=True)
124+
# Admin-authored policy text injected into every AI session's system prompt
125+
# (chat + scheduled runs). Documents WHEN to open helpdesk tickets AND HOW
126+
# (the ticketing API calls themselves) - fully dynamic, no code changes to
127+
# switch ticketing systems.
128+
ai_helpdesk_prompt = models.TextField(blank=True, default="")
129+
# Generic ticketing API access for the helpdesk_api_request tool. The AI
130+
# writes {{HELPDESK_API_KEY}} in request bodies; the bridge substitutes the
131+
# real key server-side (the key is never placed in the AI's context).
132+
ai_helpdesk_api_base_url = models.CharField(max_length=255, blank=True, default="")
133+
ai_helpdesk_api_key = models.CharField(max_length=255, blank=True, default="")
134+
# Admin-authored JS integration ("helpdesk.js") defining deterministic
135+
# operations (create_ticket, reply, note, submit_report, ...) for ANY
136+
# ticketing system. Runs on the bridge; the AI calls the operations by name.
137+
# This is the "precise code" companion to the natural-language policy above.
138+
ai_helpdesk_code = models.TextField(blank=True, default="")
124139
enable_server_scripts = models.BooleanField(default=True)
125140
enable_server_webterminal = models.BooleanField(default=False)
126141
notify_on_info_alerts = models.BooleanField(default=False)
@@ -777,6 +792,9 @@ class AITaskRun(models.Model):
777792
"agents.Agent", related_name="ai_runs", on_delete=models.CASCADE, null=True, blank=True
778793
)
779794
run_id = models.CharField(max_length=64, unique=True) # correlates live progress
795+
# groups all per-machine runs of a single bulk dispatch, so a finalizer can
796+
# compile ONE combined report after the whole batch finishes.
797+
batch_id = models.CharField(max_length=64, null=True, blank=True, db_index=True)
780798
triggered_by = models.CharField(max_length=20, default="schedule") # schedule|manual|bulk
781799
started_at = models.DateTimeField(auto_now_add=True)
782800
finished_at = models.DateTimeField(null=True, blank=True)
@@ -833,6 +851,10 @@ class BulkAICommand(BaseAuditModel):
833851

834852
name = models.CharField(max_length=255)
835853
prompt = models.TextField()
854+
# Optional: when set, after the whole batch finishes a single finalizer run
855+
# compiles ONE combined report (given every machine's result) following this
856+
# instruction + the HELPDESK POLICY. Empty = no combined report.
857+
report_prompt = models.TextField(blank=True, default="")
836858
model = models.ForeignKey(
837859
"core.AIModel", null=True, blank=True, on_delete=models.SET_NULL
838860
)

api/tacticalrmm/core/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ class AITaskRunSerializer(serializers.ModelSerializer):
206206
hostname = serializers.SerializerMethodField()
207207
client = serializers.SerializerMethodField()
208208
site = serializers.SerializerMethodField()
209+
device_id = serializers.SerializerMethodField()
209210

210211
class Meta:
211212
model = AITaskRun
@@ -223,6 +224,10 @@ def get_site(self, obj) -> str:
223224
a = obj.get_agent()
224225
return a.site.name if a else ""
225226

227+
def get_device_id(self, obj) -> str:
228+
a = obj.get_agent()
229+
return a.agent_id if a else ""
230+
226231

227232
class BulkAICommandSerializer(serializers.ModelSerializer):
228233
model_display = serializers.SerializerMethodField()

0 commit comments

Comments
 (0)