Files
replypier/backend/app/models/guardrail_event.py
T
WangShen007 20fef2b9f3 chore: settle product naming across app, widget and deployments
One word everywhere: ReplyPier in UI copy, package and image names, the
replypier.site domains (app/api/docs/help), REPLYPIER_* settings and the
--rp-* design tokens.

- public widget API is window.ReplyPier with replypierId / replypierBaseUrl /
  replypierConfig / replypierToken, loader at webclient/replypier.min.js;
  both bundles rebuilt from source
- NOTICE, llms.txt, CONTRIBUTING, HELP_CENTER_INFRA and the demo runtime
  config rewritten; the demo config ships no third-party keys
- migration adds the ReplyPier managed model value to the AI model enum
- local compose project, database, role, volume and Docker network carry the
  same name
2026-10-26 00:57:22 +08:00

90 lines
3.3 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.
"""
import uuid
from sqlalchemy import (
JSON,
TIMESTAMP,
Boolean,
Column,
Float,
ForeignKey,
Index,
Integer,
String,
UUID,
func,
)
from app.core.encryption import EncryptedText
from app.database import Base
class GuardrailEvent(Base):
"""One row per guardrail trigger across every surface (widget, channels,
workflow, help center).
Append-only: this is the record that answers "is off-topic abuse actually
material?" and "is a rule wrongly refusing real customers?" — the review
gate before any detection rule is promoted from counting to blocking.
`matched` holds rule ids / short match tokens ONLY — the column is plain
JSON (unencrypted), so raw visitor text must never land in it. The
reviewable text lives in `excerpt`, which is encrypted at rest exactly
like ChatHistory.message.
"""
__tablename__ = "guardrail_events"
__table_args__ = (
# Declared here as well as in the migration so autogenerate never
# proposes dropping them and the sqlite test schema matches Postgres.
Index("ix_guardrail_events_org_created", "organization_id", "created_at"),
Index("ix_guardrail_events_rule_created", "rule", "created_at"),
)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id = Column(
UUID(as_uuid=True),
ForeignKey("organizations.id", ondelete="CASCADE"),
nullable=True,
)
agent_id = Column(
UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True
)
# No FK: a help-center /ask event has no chat session at all.
session_id = Column(UUID(as_uuid=True), nullable=True)
# widget | channel | workflow | help_center
surface = Column(String(20), nullable=False)
# inbound | output
layer = Column(String(16), nullable=False)
# Stable rule id, e.g. injection.frame_tokens, offtopic.model_refused
rule = Column(String(64), nullable=False)
# blocked | counted | replaced
action = Column(String(16), nullable=False)
score = Column(Float, nullable=True)
# Rule ids / short match tokens only — NEVER raw visitor text (plain JSON).
matched = Column(JSON, default=None, nullable=True)
# Input length so wasted-token cost is estimable (~ char_len / 4).
char_len = Column(Integer, nullable=True)
# First 300 chars of the offending text, encrypted at rest; gated by
# GUARDRAIL_STORE_EXCERPT.
excerpt = Column(EncryptedText, nullable=True)
# Review workflow: precision denominator for the strict-mode decision.
reviewed = Column(Boolean, default=False, nullable=False)
false_positive = Column(Boolean, default=False, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())