The upstream project name is gone from code, assets, docs and build
output: brand strings, domains (replypier.site and its app/api/docs/help
subdomains), env prefixes, image and network names, the widget's public
JS API (window.ReplyPier, replypierId/BaseUrl/Config/Token), CSS tokens
(--rp-*), file and directory names, and the WordPress plugin package.
- widget loader is now webclient/replypier.min.js; both bundles rebuilt
from source (backend/assets/widget.{js,css}, frontend/public/webclient)
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and config.dev.js
rewritten rather than string-swapped; the demo config no longer ships
the previous vendor's Firebase/Razorpay/Google keys
- new migration replypier_ai_model_001 adds REPLYPIER to the managed
AIModelType and rewrites pre-rename rows
- local compose project, database, role and volume renamed; LOCAL_DEPLOY
documents carrying the old local data over
126 lines
5.8 KiB
Python
126 lines
5.8 KiB
Python
"""
|
|
Copyright 2024-2026 ReplyPier
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
The off-topic exercise detector exists because prompt text demonstrably failed:
|
|
a long algorithms brief was answered in full in production under three
|
|
successive policy wordings. It is deliberately narrow, so these tests weigh
|
|
PRECISION far above recall — a wrongly blocked customer is worse than a missed
|
|
exercise, which the prompt policy still catches on the way through.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.utils.guardrails import detect_offtopic_exercise
|
|
|
|
# The exact brief that got through in production on 2026-07-31.
|
|
PROD_BRIEF = """Problem: Median in a Stream with Deletions
|
|
Design a data structure that supports the following operations, each in O(log n)
|
|
time: insert(x) — add value x to the collection; delete(x) — remove one
|
|
occurrence of value x from the collection (guaranteed to exist); find_median() —
|
|
return the median of all currently present values in O(1) time.
|
|
Constraints: Values are not necessarily distinct (duplicates allowed). n can be
|
|
up to 10^6, and operations are interleaved arbitrarily.
|
|
The naive two-heap median trick handles insert and find_median well, but breaks
|
|
down for delete.
|
|
Your task: Design a structure that supports all three operations within the time
|
|
bounds. State and justify the time and space complexity of each operation.
|
|
Bonus: how would your design change if you also needed find_kth(k)?"""
|
|
|
|
# Long, technical, and unmistakably about a real product.
|
|
LONG_INTEGRATION_QUESTION = """We're rolling your widget out across three
|
|
storefronts and I want to get the architecture right before we commit.
|
|
Each store has its own subdomain and its own knowledge base, and we'd like a
|
|
single dashboard view across all of them. Right now we install the widget
|
|
script per store, but the api key seems to be scoped per organisation rather
|
|
than per site, so I can't tell the conversations apart in the inbox.
|
|
Is there a recommended setup for multi-brand deployments? Should we create one
|
|
organisation per storefront and accept separate billing, or is there a way to
|
|
segment within one account? We also need the webhook payloads to identify the
|
|
originating store so our CRM can route them.""" + " padding. " * 20
|
|
|
|
LONG_TRACEBACK = """The worker keeps dying in production and I can't work out why.
|
|
Here is what we get, repeated every few minutes:
|
|
Traceback (most recent call last):
|
|
File "app/worker.py", line 42, in <module>
|
|
client.connect()
|
|
File "app/net.py", line 118, in connect
|
|
raise ConnectionRefusedError(errno.ECONNREFUSED, os.strerror(errno.ECONNREFUSED))
|
|
ConnectionRefusedError: [Errno 111] Connection refused
|
|
This started after we moved the container to a new host. The docker compose
|
|
setup is unchanged and redis is definitely running. Any idea what we should
|
|
check first? It fails on every restart.""" + " More detail follows. " * 12
|
|
|
|
|
|
class TestCatchesTheAbuse:
|
|
def test_the_production_brief(self):
|
|
assert detect_offtopic_exercise(PROD_BRIEF) is True
|
|
|
|
def test_still_caught_when_org_name_is_known(self):
|
|
assert detect_offtopic_exercise(PROD_BRIEF, ["ReplyPier"]) is True
|
|
|
|
|
|
class TestPrecision:
|
|
"""Everything here is legitimate traffic and must never be caught."""
|
|
|
|
def test_long_product_question_with_business_anchors(self):
|
|
assert detect_offtopic_exercise(LONG_INTEGRATION_QUESTION) is False
|
|
|
|
def test_long_pasted_traceback(self):
|
|
assert detect_offtopic_exercise(LONG_TRACEBACK) is False
|
|
|
|
def test_exercise_shaped_but_about_the_product(self):
|
|
"""A genuine scaling question that happens to use the same vocabulary."""
|
|
text = (
|
|
"We push about 10^6 conversations a month through your webhook and "
|
|
"the ordering matters to us. What data structure do you use "
|
|
"internally for the queue, and what is the time complexity of a "
|
|
"replay? We need worst-case guarantees before we design around it. "
|
|
"Constraints: we cannot lose events, and duplicates allowed is not "
|
|
"acceptable for our billing reconciliation."
|
|
) + " Extra context. " * 20
|
|
assert detect_offtopic_exercise(text) is False
|
|
|
|
def test_short_exercise_is_left_to_the_prompt(self):
|
|
"""Below the length bar the policy block handles it — the detector is
|
|
for the long briefs the prompt cannot hold."""
|
|
short = (
|
|
"Design a data structure with insert and getRandom in O(1). "
|
|
"Implement the class and give the time complexity."
|
|
)
|
|
assert detect_offtopic_exercise(short) is False
|
|
|
|
@pytest.mark.parametrize("text", [
|
|
"hi",
|
|
"",
|
|
"How much does the Pro plan cost per seat, and is there a free trial?",
|
|
"sudo docker compose up -d fails with permission denied, any ideas?",
|
|
])
|
|
def test_ordinary_messages(self, text):
|
|
assert detect_offtopic_exercise(text) is False
|
|
|
|
def test_none_is_safe(self):
|
|
assert detect_offtopic_exercise(None) is False
|
|
|
|
|
|
class TestThresholds:
|
|
def test_needs_enough_markers(self):
|
|
"""Long and technical is not enough on its own."""
|
|
text = ("We use an algorithm to sort the results. " + "Filler text here. " * 40)
|
|
assert detect_offtopic_exercise(text) is False
|
|
|
|
def test_org_name_mention_anchors_it(self):
|
|
text = PROD_BRIEF + "\nWe need this for our ReplyPier integration."
|
|
assert detect_offtopic_exercise(text, ["ReplyPier"]) is False
|