Aug 12 - Fix AI support bugs
This commit is contained in:
+86
-23
@@ -101,9 +101,20 @@ to third parties. They download the app, complete the enrollment form, and they
|
|||||||
ready to go.
|
ready to go.
|
||||||
Such a person is enrolled with an inspecting role of their own (rather than the \
|
Such a person is enrolled with an inspecting role of their own (rather than the \
|
||||||
read-only customer portal role), scoped to that customer's own contracts and facilities. \
|
read-only customer portal role), scoped to that customer's own contracts and facilities. \
|
||||||
Enrollment is the JQC Enrollment Form, reachable from the About Us page ("Enroll More \
|
See ENROLLING MORE PEOPLE below for the form.
|
||||||
People"), where you list each person, their role, and what they should be able to do. \
|
|
||||||
Each person then receives an email invitation to set up their own username and password.
|
=== ENROLLING MORE PEOPLE (adding users) ===
|
||||||
|
To add colleagues to JQC, use the JQC Enrollment Form:
|
||||||
|
https://jqc.ltservicesinc.com/enrollment
|
||||||
|
It is also linked from the About Us page in the portal ("Enroll More People"). No login \
|
||||||
|
is needed to fill it in, so it can be forwarded to anyone who needs to be set up.
|
||||||
|
On the form you list each person (name, job title, email and the role they need), tick \
|
||||||
|
what each of them should be able to do, and say who should get the mobile app. After \
|
||||||
|
submitting, the requester receives a confirmation email with a reference number, and \
|
||||||
|
each person listed then receives their own email invitation to choose a username and \
|
||||||
|
password.
|
||||||
|
When a customer asks how to add a user, get the app, or give someone inspecting access, \
|
||||||
|
give them this URL. It is the correct link — do not alter it or invent another one.
|
||||||
|
|
||||||
=== GETTING HUMAN HELP ===
|
=== GETTING HUMAN HELP ===
|
||||||
If the customer needs something this chat can't resolve — an access/login problem, a \
|
If the customer needs something this chat can't resolve — an access/login problem, a \
|
||||||
@@ -113,11 +124,16 @@ opens a request that the provider's admin team answers by email and in "My Reque
|
|||||||
|
|
||||||
=== STYLE & RULES ===
|
=== STYLE & RULES ===
|
||||||
- Be concise, warm, and practical. Prefer short paragraphs or numbered steps.
|
- Be concise, warm, and practical. Prefer short paragraphs or numbered steps.
|
||||||
- Ground answers in the features above. If you are not sure or the app may differ, say \
|
- Ground answers in everything above, INCLUDING the "ADDITIONAL KNOWLEDGE" section when \
|
||||||
so honestly rather than guessing — and suggest "Submit to Support".
|
one is present — that section is curated by the JQC team and is authoritative. If it \
|
||||||
|
answers the question, use it. If you are not sure or the app may differ, say so \
|
||||||
|
honestly rather than guessing — and suggest "Submit to Support".
|
||||||
|
- When the knowledge above contains a link (URL), email address or exact wording, quote \
|
||||||
|
it EXACTLY as written. Repeating something given to you here is not inventing — do it \
|
||||||
|
freely. Never alter a URL, shorten it, or replace it with a description.
|
||||||
- NEVER invent specific staff names, contract prices, cleaning schedules, phone numbers, \
|
- NEVER invent specific staff names, contract prices, cleaning schedules, phone numbers, \
|
||||||
facility data, or scores. You do not have access to the customer's live data — guide \
|
facility data, or scores that are NOT given above. You do not have access to the \
|
||||||
them to where to find it in the portal instead.
|
customer's live data — guide them to where to find it in the portal instead.
|
||||||
- Do not claim to perform actions yourself; explain where in the portal the customer does it.\
|
- Do not claim to perform actions yourself; explain where in the portal the customer does it.\
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -166,30 +182,55 @@ def _redact_pii(text):
|
|||||||
return redacted
|
return redacted
|
||||||
|
|
||||||
|
|
||||||
|
#: The curated knowledge is spliced in immediately BEFORE this heading, not
|
||||||
|
#: appended to the end of the prompt. The rules under it say "ground answers in
|
||||||
|
#: everything above", so knowledge appended after them was, by the prompt's own
|
||||||
|
#: instruction, out of scope — which is exactly why admin KB entries appeared to
|
||||||
|
#: be ignored. Keep this marker in sync with the heading in _SYSTEM_PROMPT.
|
||||||
|
_STYLE_MARKER = '=== STYLE & RULES ==='
|
||||||
|
|
||||||
|
|
||||||
def _system_prompt_with_kb():
|
def _system_prompt_with_kb():
|
||||||
"""Return the base system prompt plus all ACTIVE admin knowledge entries
|
"""Return the base system prompt with all ACTIVE admin knowledge entries
|
||||||
(phase38), so staff can curate the chatbot's knowledge without code changes.
|
(phase38) spliced in, so staff can curate the chatbot's knowledge without
|
||||||
Best-effort — a KB failure never breaks the chat."""
|
code changes. Best-effort — a KB failure never breaks the chat."""
|
||||||
prompt = _SYSTEM_PROMPT
|
|
||||||
try:
|
try:
|
||||||
entries = (SupportKnowledge.query
|
entries = (SupportKnowledge.query
|
||||||
.filter_by(active=True)
|
.filter_by(active=True)
|
||||||
.order_by(SupportKnowledge.sort_order.asc(), SupportKnowledge.id.asc())
|
.order_by(SupportKnowledge.sort_order.asc(), SupportKnowledge.id.asc())
|
||||||
.all())
|
.all())
|
||||||
if entries:
|
if not entries:
|
||||||
parts = ["\n\n=== ADDITIONAL KNOWLEDGE (curated by the JQC team; "
|
logger.info('SUPPORT | KB | no active entries — base prompt only')
|
||||||
"treat as authoritative and prefer it over general guesses) ==="]
|
return _SYSTEM_PROMPT
|
||||||
total = 0
|
|
||||||
for e in entries:
|
parts = ['=== ADDITIONAL KNOWLEDGE (curated by the JQC team; authoritative — '
|
||||||
block = f"\n\nTopic: {e.title}\n{e.content.strip()}"
|
'prefer it over general guesses, and quote any link in it exactly) ===']
|
||||||
if total + len(block) > _KB_MAX_CHARS:
|
total = 0
|
||||||
break
|
used = 0
|
||||||
parts.append(block)
|
for e in entries:
|
||||||
total += len(block)
|
block = f"\n\nTopic: {e.title}\n{e.content.strip()}"
|
||||||
prompt += ''.join(parts)
|
if total + len(block) > _KB_MAX_CHARS:
|
||||||
|
logger.warning('SUPPORT | KB | %d of %d entries dropped — %d char cap '
|
||||||
|
'reached', len(entries) - used, len(entries), _KB_MAX_CHARS)
|
||||||
|
break
|
||||||
|
parts.append(block)
|
||||||
|
total += len(block)
|
||||||
|
used += 1
|
||||||
|
kb_block = ''.join(parts)
|
||||||
|
|
||||||
|
idx = _SYSTEM_PROMPT.find(_STYLE_MARKER)
|
||||||
|
if idx == -1: # marker renamed — fall back to append
|
||||||
|
logger.warning('SUPPORT | KB | style marker not found; appending at end')
|
||||||
|
prompt = f'{_SYSTEM_PROMPT}\n\n{kb_block}'
|
||||||
|
else:
|
||||||
|
prompt = (f'{_SYSTEM_PROMPT[:idx]}{kb_block}\n\n{_SYSTEM_PROMPT[idx:]}')
|
||||||
|
|
||||||
|
logger.info('SUPPORT | KB | %d/%d entries injected (%d chars), prompt=%d chars',
|
||||||
|
used, len(entries), total, len(prompt))
|
||||||
|
return prompt
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning('SUPPORT | knowledge-base load failed: %s', exc)
|
logger.warning('SUPPORT | knowledge-base load failed: %s', exc)
|
||||||
return prompt
|
return _SYSTEM_PROMPT
|
||||||
|
|
||||||
|
|
||||||
# ── Customer chat page ────────────────────────────────────────────────────────
|
# ── Customer chat page ────────────────────────────────────────────────────────
|
||||||
@@ -571,6 +612,28 @@ def admin_knowledge():
|
|||||||
entries=entries, groq_ready=groq_ready)
|
entries=entries, groq_ready=groq_ready)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/admin/knowledge/preview')
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def admin_knowledge_preview():
|
||||||
|
"""Show the exact system prompt the chatbot receives, knowledge included.
|
||||||
|
|
||||||
|
Added after admin entries appeared to be ignored: without this there is no
|
||||||
|
way to tell "my entry never reached the prompt" from "the model saw it and
|
||||||
|
chose not to use it". Read-only, builds nothing of its own — it calls the
|
||||||
|
same _system_prompt_with_kb() the chat endpoint calls.
|
||||||
|
"""
|
||||||
|
prompt = _system_prompt_with_kb()
|
||||||
|
active_count = SupportKnowledge.query.filter_by(active=True).count()
|
||||||
|
total_count = SupportKnowledge.query.count()
|
||||||
|
return render_template('support/admin_knowledge_preview.html',
|
||||||
|
prompt=prompt,
|
||||||
|
active_count=active_count,
|
||||||
|
total_count=total_count,
|
||||||
|
kb_included='=== ADDITIONAL KNOWLEDGE' in prompt,
|
||||||
|
kb_cap=_KB_MAX_CHARS)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/admin/knowledge/new', methods=['GET', 'POST'])
|
@bp.route('/admin/knowledge/new', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@supervisor_required
|
@supervisor_required
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
<a href="{{ url_for('support.admin_tickets') }}" class="btn btn-outline-secondary btn-sm">
|
<a href="{{ url_for('support.admin_tickets') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-life-preserver me-1"></i>Support Tickets
|
<i class="bi bi-life-preserver me-1"></i>Support Tickets
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('support.admin_knowledge_preview') }}" class="btn btn-outline-primary btn-sm"
|
||||||
|
title="See the exact prompt the chatbot receives, with your entries in it">
|
||||||
|
<i class="bi bi-eye me-1"></i>What the AI Sees
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('support.admin_knowledge_new') }}" class="btn btn-primary btn-sm">
|
<a href="{{ url_for('support.admin_knowledge_new') }}" class="btn btn-primary btn-sm">
|
||||||
<i class="bi bi-plus-circle me-1"></i>Add Entry
|
<i class="bi bi-plus-circle me-1"></i>Add Entry
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}What the AI Sees{% endblock %}
|
||||||
|
|
||||||
|
{# Read-only view of the assembled system prompt. Exists so an admin can tell
|
||||||
|
"my knowledge entry never reached the prompt" apart from "the model saw it
|
||||||
|
and chose not to use it" — the two have completely different fixes. #}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
|
||||||
|
<h2 class="mb-0"><i class="bi bi-eye"></i> What the AI Sees</h2>
|
||||||
|
<a href="{{ url_for('support.admin_knowledge') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Back to Knowledge Base
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center py-3">
|
||||||
|
<div class="fs-4 fw-bold">{{ active_count }}</div>
|
||||||
|
<div class="text-muted small">Active entries</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center py-3">
|
||||||
|
<div class="fs-4 fw-bold">{{ total_count - active_count }}</div>
|
||||||
|
<div class="text-muted small">Inactive (not sent)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center py-3">
|
||||||
|
<div class="fs-4 fw-bold">{{ prompt | length }}</div>
|
||||||
|
<div class="text-muted small">Prompt characters</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center py-3">
|
||||||
|
{% if kb_included %}
|
||||||
|
<div class="fs-4 fw-bold text-success"><i class="bi bi-check-circle"></i></div>
|
||||||
|
<div class="text-muted small">Knowledge included</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="fs-4 fw-bold text-danger"><i class="bi bi-x-circle"></i></div>
|
||||||
|
<div class="text-muted small">Knowledge NOT included</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not kb_included and total_count %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
|
You have {{ total_count }} knowledge entr{{ 'y' if total_count == 1 else 'ies' }}, but
|
||||||
|
none reached the prompt. Check that at least one is marked <strong>Active</strong>.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
This is the exact text sent to the AI ahead of every customer question. Entries are
|
||||||
|
capped at {{ kb_cap }} characters in total — past that, later entries are dropped
|
||||||
|
(lowest sort order is kept first). If something you wrote appears here but the AI still
|
||||||
|
will not say it, the wording of the entry is the thing to change, not the setup.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light fw-semibold">Assembled system prompt</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<pre class="mb-0 p-3" style="white-space:pre-wrap; font-size:.8rem; max-height:70vh;
|
||||||
|
overflow-y:auto; background:#f8fafc;">{{ prompt }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user