Aug 12 - Fix AI support bugs
This commit is contained in:
+79
-16
@@ -101,9 +101,20 @@ to third parties. They download the app, complete the enrollment form, and they
|
||||
ready to go.
|
||||
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. \
|
||||
Enrollment is the JQC Enrollment Form, reachable from the About Us page ("Enroll More \
|
||||
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.
|
||||
See ENROLLING MORE PEOPLE below for the form.
|
||||
|
||||
=== 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 ===
|
||||
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 ===
|
||||
- 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 \
|
||||
so honestly rather than guessing — and suggest "Submit to Support".
|
||||
- Ground answers in everything above, INCLUDING the "ADDITIONAL KNOWLEDGE" section when \
|
||||
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, \
|
||||
facility data, or scores. You do not have access to the customer's live data — guide \
|
||||
them to where to find it in the portal instead.
|
||||
facility data, or scores that are NOT given above. You do not have access to the \
|
||||
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.\
|
||||
"""
|
||||
|
||||
@@ -166,30 +182,55 @@ def _redact_pii(text):
|
||||
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():
|
||||
"""Return the base system prompt plus all ACTIVE admin knowledge entries
|
||||
(phase38), so staff can curate the chatbot's knowledge without code changes.
|
||||
Best-effort — a KB failure never breaks the chat."""
|
||||
prompt = _SYSTEM_PROMPT
|
||||
"""Return the base system prompt with all ACTIVE admin knowledge entries
|
||||
(phase38) spliced in, so staff can curate the chatbot's knowledge without
|
||||
code changes. Best-effort — a KB failure never breaks the chat."""
|
||||
try:
|
||||
entries = (SupportKnowledge.query
|
||||
.filter_by(active=True)
|
||||
.order_by(SupportKnowledge.sort_order.asc(), SupportKnowledge.id.asc())
|
||||
.all())
|
||||
if entries:
|
||||
parts = ["\n\n=== ADDITIONAL KNOWLEDGE (curated by the JQC team; "
|
||||
"treat as authoritative and prefer it over general guesses) ==="]
|
||||
if not entries:
|
||||
logger.info('SUPPORT | KB | no active entries — base prompt only')
|
||||
return _SYSTEM_PROMPT
|
||||
|
||||
parts = ['=== ADDITIONAL KNOWLEDGE (curated by the JQC team; authoritative — '
|
||||
'prefer it over general guesses, and quote any link in it exactly) ===']
|
||||
total = 0
|
||||
used = 0
|
||||
for e in entries:
|
||||
block = f"\n\nTopic: {e.title}\n{e.content.strip()}"
|
||||
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)
|
||||
prompt += ''.join(parts)
|
||||
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:
|
||||
logger.warning('SUPPORT | knowledge-base load failed: %s', exc)
|
||||
return prompt
|
||||
return _SYSTEM_PROMPT
|
||||
|
||||
|
||||
# ── Customer chat page ────────────────────────────────────────────────────────
|
||||
@@ -571,6 +612,28 @@ def admin_knowledge():
|
||||
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'])
|
||||
@login_required
|
||||
@supervisor_required
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<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
|
||||
</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">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Entry
|
||||
</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