diff --git a/app/routes/support.py b/app/routes/support.py index 56b9ba2..edce8dd 100644 --- a/app/routes/support.py +++ b/app/routes/support.py @@ -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) ==="] - total = 0 - for e in entries: - block = f"\n\nTopic: {e.title}\n{e.content.strip()}" - if total + len(block) > _KB_MAX_CHARS: - break - parts.append(block) - total += len(block) - prompt += ''.join(parts) + 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) + 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 diff --git a/app/templates/support/admin_knowledge.html b/app/templates/support/admin_knowledge.html index 1d62ea7..4abfba8 100644 --- a/app/templates/support/admin_knowledge.html +++ b/app/templates/support/admin_knowledge.html @@ -11,6 +11,10 @@ Support Tickets + + What the AI Sees + Add Entry diff --git a/app/templates/support/admin_knowledge_preview.html b/app/templates/support/admin_knowledge_preview.html new file mode 100644 index 0000000..ba25599 --- /dev/null +++ b/app/templates/support/admin_knowledge_preview.html @@ -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 %} +
+

What the AI Sees

+ + Back to Knowledge Base + +
+ +
+
+
+
+
{{ active_count }}
+
Active entries
+
+
+
+
+
+
+
{{ total_count - active_count }}
+
Inactive (not sent)
+
+
+
+
+
+
+
{{ prompt | length }}
+
Prompt characters
+
+
+
+
+
+
+ {% if kb_included %} +
+
Knowledge included
+ {% else %} +
+
Knowledge NOT included
+ {% endif %} +
+
+
+
+ +{% if not kb_included and total_count %} +
+ + 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 Active. +
+{% endif %} + +
+ + 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. +
+ +
+
Assembled system prompt
+
+
{{ prompt }}
+
+
+{% endblock %}