[pending-judge] feat(DC-163): AI assistant that works — LLM intent engine + fixed chat panel
The floating assistant was broken end-to-end; Sami said "Do it that works".
Three user-visible bugs fixed + the promised LLM delegation implemented:
FRONTEND (status/js/ai-chat.js — rewritten):
- Response parsing: old code read data.data; the API returns the FLAT ok()
envelope — EVERY reply rendered "Sorry, something went wrong". Now reads
the real shape (contract asserted by route test).
- Suggestion chips: onclick="...send('"Deploy Plex"')" — nested quotes,
invalid JS, dead buttons (shipped like this). Now data-driven buttons with
addEventListener.
- Deploy button: called window.openAppSelector which NEVER existed anywhere
in the tree (grep-verified). Now opens the REAL deploy modal via new
window.openDeployConfigFor(templateId) exported by app-selector.js —
resolves against the live API template cache, reuses showDeployConfig
(same path a manual catalog click takes). Widgets route to the grid.
- Conversation history (last 6 turns) now rides along per request, engine
badge in header, ⚙ settings sub-panel (baseUrl/model/API key + Test +
Remove), all server text via textContent (no HTML injection), CSP-safe
(no inline handlers).
BACKEND (routes/ai-intent.js):
- TWO engines, one contract. LLM classifier (OpenAI-compatible chat
completions) with system prompt embedding the LIVE 78-app catalog + the
six wizard categories; regex patterns kept as the zero-config default and
the automatic failover (HTTP error, timeout via AbortController,
unparseable output -> pattern answers; response carries engine +
llmError so the UI can say which served — no silent degradation).
- Closed-vocabulary schema validation: intent MUST be in the known set else
'unknown'; appId MUST exist in APP_TEMPLATES else nulled (hallucinated
ids cannot leak into deploy plans); categories filtered to the wizard's
real keys; reply stripped of control chars + bounded to 600; history
role-filtered (only user/assistant pass — client-injected system turns
dropped) + capped; input capped to 1000 chars.
- enrich() rebuilds the response contract per intent; pattern-canned
response is dropped when the LLM rerouted the intent (no stale shape);
deployPlan body.port now fills from the template's defaultPort (was
hardcoded null); deployPlan shape asserted against wizard usage.
- Key storage: PUT /ai/settings stores {baseUrl,model,apiKey} in the
ENCRYPTED credential store (key 'ai:llm') via ctx.credentialManager —
NOT config.json (GET /config echoes the raw file to the browser; that
route's shape was verified before this decision). Blank apiKey on PUT
keeps the stored one. GET /ai/settings reports configured/source/model/
hasApiKey — the key value NEVER crosses the wire (asserted). Env
fallback AI_LLM_BASE_URL/AI_LLM_API_KEY/AI_LLM_MODEL + legacy
LLM_PROXY_URL for headless installs. POST /ai/settings/test = one-shot
real classify probe returning ok/sample or ok:false/reason (never
throws). DELETE clears.
- GET /ai/capabilities now reports engine + llm status (pattern was honest
marketing before; now it can be true).
MOUNT (src/app.js): aiIntentRoutes receives siteConfig, log, fetchImpl
(ctx.fetchT — test-injectable) and credentialManager.
Docs: OpenAPI +4 AI path entries (intent, capabilities, settings, settings/test); CHANGELOG entry; dist bundle rebuilt
(openDeployConfigFor present in features.js; SW cache c62511c40d).
Tests: 20 NEW in __tests__/routes/ai-intent-engine.test.js (engine
selection w/ and w/o store, off-catalog phrasing via mock LLM,
hallucinated-id nulling, illegal intent coercion, HTTP-500 failover,
AbortError timeout failover, markdown-fence repair, reply sanitization,
history cap+role filter, env config path, intent-disagreement rebuild w/
real jellyfin port 8096, flat envelope contract, 400s, 1000-char cap,
settings GET/PUT/DELETE/test incl. key-never-echoed assertions,
capabilities engine reporting) + the 18 original pattern tests unchanged +
composition-root e2e 8 pass. Full suite: 3643 passed; only the pre-existing
pdf-parse billing family red (6 failed in documented band [5,7], patch
touches no billing file).
This commit is contained in:
9 files changed
+1313
-293
No files matched your search
@@ -7773,6 +7773,71 @@ paths:
|
||||
'200':
|
||||
description: Probe result (ok flag inside — transport errors are reported, not thrown)
|
||||
|
||||
/api/v1/ai/intent:
|
||||
post:
|
||||
tags: [AI]
|
||||
summary: Natural language → structured intent
|
||||
description: >
|
||||
Dual-engine intent router. LLM engine (OpenAI-compatible classifier,
|
||||
configured via /ai/settings or AI_LLM_* env) resolves against the
|
||||
full app catalog and conversation history; pattern engine serves
|
||||
offline and as automatic failover (engine field tells which answered;
|
||||
llmError records the failover reason). Output is schema-validated
|
||||
against closed vocabularies — the model can never emit an action or
|
||||
template id outside the platform's real sets. Response is the flat
|
||||
ok() envelope: {success, intent, confidence, action, engine, appId?,
|
||||
serviceId?, response{message,appId?,appName?,recommendations?,
|
||||
suggestions?,disclaimer?}, deployPlan?, wizardCall?, timestamp}.
|
||||
responses:
|
||||
'200':
|
||||
description: Structured intent (flat envelope)
|
||||
'400':
|
||||
description: message (non-empty string) required
|
||||
|
||||
/api/v1/ai/capabilities:
|
||||
get:
|
||||
tags: [AI]
|
||||
summary: What the assistant can do + live engine status
|
||||
responses:
|
||||
'200':
|
||||
description: Intent list, capabilities, engine (llm|pattern), llm config status (key never included)
|
||||
|
||||
/api/v1/ai/settings:
|
||||
get:
|
||||
tags: [AI]
|
||||
summary: LLM assistant configuration (key never echoed)
|
||||
responses:
|
||||
'200':
|
||||
description: configured, source, baseUrl, model, hasApiKey
|
||||
put:
|
||||
tags: [AI]
|
||||
summary: Store LLM config in the encrypted credential store
|
||||
description: >
|
||||
body {baseUrl?, model?, apiKey?}. Stored under credential key
|
||||
'ai:llm' — NEVER config.json (GET /config returns the raw file to
|
||||
the browser). Blank apiKey keeps the stored one. First save
|
||||
requires baseUrl + apiKey. baseUrl must be http(s).
|
||||
responses:
|
||||
'200':
|
||||
description: Saved (response never contains the key)
|
||||
'400':
|
||||
description: Invalid baseUrl or missing required fields
|
||||
delete:
|
||||
tags: [AI]
|
||||
summary: Remove stored LLM config (falls back to env/pattern)
|
||||
responses:
|
||||
'200':
|
||||
description: Cleared
|
||||
|
||||
/api/v1/ai/settings/test:
|
||||
post:
|
||||
tags: [AI]
|
||||
summary: One-shot classifier probe (proves the LLM end-to-end)
|
||||
description: Sends a fixed sample message through the configured LLM and returns the parsed intent. Failures answer ok:false with a reason (200), never throw.
|
||||
responses:
|
||||
'200':
|
||||
description: '{ok, engine, model?, sample?{intent,reply}} or {ok:false, reason}'
|
||||
|
||||
/api/v1/mcp:
|
||||
post:
|
||||
tags: [MCP]
|
||||
|
||||
Reference in new issue
Block a user