Download src/agents/refusals.py from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 7.11 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/94de89b331583415f150d69415b964754bb481bc/src/agents/refusals.py
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog@94de89b331583415f150d69415b964754bb481bc/src/agents/refusals.py
-
curl -L -o refusals.py https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/94de89b331583415f150d69415b964754bb481bc/src/agents/refusals.py
7.11 kB
| """Canned, language-matched refusals for the guardrail layer. | |
| Centralized so the three refusal sites β the `out_of_scope` router branch, the | |
| input-guard block, and the content-filter fallback β speak with one voice. | |
| Deterministic on purpose (no LLM): the refusal must not call the same model that | |
| just refused or failed. Language is picked with a tiny marker check (EN default, | |
| ID when Indonesian markers are present), mirroring the lightweight detector in | |
| `handlers/help.py` β enough for a one-line reply without an LLM round-trip. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| # Subset of help.py's ID marker set β enough to pick EN vs ID for a short refusal. | |
| # Includes the common Indonesian injection verbs ("lupakan"=forget, "abaikan"=ignore) | |
| # so an ID-language jailbreak still gets an ID-language refusal. | |
| _ID_MARKERS = frozenset({ | |
| "yang", "dan", "apa", "gimana", "bagaimana", "kenapa", "mengapa", "aku", "saya", | |
| "tolong", "ini", "itu", "untuk", "dengan", "tidak", "nggak", "enggak", "bisa", | |
| "mau", "buat", "dari", "kamu", "berapa", "kapan", "siapa", "dimana", "adalah", | |
| "akan", "sudah", "belum", "lupakan", "abaikan", "kredensial", "tunjukkan", | |
| }) | |
| def _is_indonesian(message: str) -> bool: | |
| tokens = re.findall(r"[a-z']+", (message or "").lower()) | |
| return any(t in _ID_MARKERS for t in tokens) | |
| # Off-topic / out-of-scope: the request is benign but outside what the assistant does. | |
| _OUT_OF_SCOPE = { | |
| "en": ( | |
| "That's outside what I can help with β I'm a data assistant, so I can only work " | |
| "with the sources you've connected to Data Eyond. Ask me a question about your " | |
| "data, or type /help to see what I can do." | |
| ), | |
| "id": ( | |
| "Itu di luar yang bisa saya bantu β saya asisten data, jadi saya hanya bisa " | |
| "bekerja dengan sumber data yang Anda hubungkan ke Data Eyond. Ajukan pertanyaan " | |
| "tentang data Anda, atau ketik /help untuk melihat yang bisa saya lakukan." | |
| ), | |
| } | |
| # Blocked: a manipulation / injection / secret-extraction / abuse attempt. | |
| _BLOCKED = { | |
| "en": ( | |
| "I can't help with that request. I'm here to analyze the data you've connected β " | |
| "ask me a question about your data and I'll take it from there." | |
| ), | |
| "id": ( | |
| "Saya tidak bisa membantu permintaan tersebut. Saya di sini untuk menganalisis " | |
| "data yang Anda hubungkan β ajukan pertanyaan tentang data Anda dan saya bantu." | |
| ), | |
| } | |
| # Data-gap: the planner judged the bound sources cannot answer the question | |
| # (planner.md "When the catalog cannot answer"). Deterministic wrapper on | |
| # purpose β the model that declined to plan is not re-asked to prose it up. | |
| # Keyed on the pipeline's reply_language ("Indonesian"/"English"), not marker | |
| # detection: the upstream language decision is authoritative here. | |
| _DATA_GAP = { | |
| "en": ( | |
| "I can't answer that from the data sources connected to this analysis. " | |
| "{reason}You can bind a source that holds this data, or ask me what's " | |
| "available (try /help or \"what data do I have?\")." | |
| ), | |
| "id": ( | |
| "Saya tidak bisa menjawab itu dari sumber data yang terhubung ke " | |
| "analisis ini. {reason}Anda bisa menambahkan sumber yang memuat data " | |
| "tersebut, atau tanyakan data apa yang tersedia (coba /help atau " | |
| "\"data apa yang saya punya?\")." | |
| ), | |
| } | |
| # Run failure: the plan was feasible but EVERY execution step failed (S1a CK1, | |
| # SPINE_V2_PLAN Β§3). Deterministic like the data-gap answer β a run that just | |
| # failed is not re-asked to prose up its own failure. Same reply_language keying. | |
| _RUN_FAILURE = { | |
| "en": ( | |
| "I ran the analysis, but every step failed, so I don't have a result I can " | |
| "stand behind. {reason}You can try rephrasing the question, or check that " | |
| "the connected data source is reachable." | |
| ), | |
| "id": ( | |
| "Saya sudah menjalankan analisisnya, tetapi semua langkah gagal, jadi belum " | |
| "ada hasil yang bisa saya laporkan. {reason}Coba ubah pertanyaannya, atau " | |
| "periksa apakah sumber data yang terhubung dapat diakses." | |
| ), | |
| } | |
| # Markers of a raw driver/SQL error. Such a reason quoted the full statement and | |
| # psycopg's text into the answer (SDJ FE test 2026-10-27); the FE then rendered its | |
| # `$...$` as maths. It stays in the record's caveats and the logs, not the reply. | |
| _RAW_ERROR_MARKERS = ("[SQL:", "[parameters:", "psycopg", "sqlalche.me", "Traceback", "LINE ") | |
| _MAX_REASON_CHARS = 200 | |
| def _presentable(reason: str | None) -> str: | |
| """The failure detail if it reads as a sentence for a user, else ''.""" | |
| detail = (reason or "").strip() | |
| if ( | |
| not detail | |
| or "\n" in detail | |
| or len(detail) > _MAX_REASON_CHARS | |
| or any(m in detail for m in _RAW_ERROR_MARKERS) | |
| ): | |
| return "" | |
| return detail | |
| def run_failure_message(reason: str | None, reply_language: str | None = None) -> str: | |
| """Answer for an all-steps-failed run (S1a CK1). `reason` is the most specific | |
| failure detail available (may be None); `reply_language` as in data_gap_message.""" | |
| detail = _presentable(reason) | |
| if detail and not detail.endswith((".", "!", "?")): | |
| detail += "." | |
| lang = "id" if reply_language == "Indonesian" else "en" | |
| return _RUN_FAILURE[lang].format(reason=f"{detail} " if detail else "") | |
| _DATA_GAP_LEAD = { | |
| "en": "I can't answer that from the data sources connected to this analysis. ", | |
| "id": "Saya tidak bisa menjawab itu dari sumber data yang terhubung ke analisis ini. ", | |
| } | |
| def data_gap_message(reason: str | None, reply_language: str | None = None) -> str: | |
| """Answer for an infeasible analysis: the bound sources lack the asked-for data. | |
| `reason` is the planner's `infeasible_reason` (may be None/empty); | |
| `reply_language` is the pipeline's detected language ("Indonesian"/"English"). | |
| The planner's reason is written FOR the user (planner.md: what is missing and the | |
| nearest data that is available), so it is not held to the 200-char cap meant for | |
| raw execution errors: that cap dropped all 18 of 18 reasons in the 2026-10-28 eval | |
| runs (246β502 chars) and left only the generic "add a source" advice (2026-10-29). | |
| Raw driver text is still never quoted. The generic advice is shown only when there | |
| is no reason to show. | |
| """ | |
| detail = (reason or "").strip() | |
| if any(m in detail for m in _RAW_ERROR_MARKERS): | |
| detail = "" | |
| lang = "id" if reply_language == "Indonesian" else "en" | |
| if not detail: | |
| return _DATA_GAP[lang].format(reason="") | |
| if not detail.endswith((".", "!", "?")): | |
| detail += "." | |
| return _DATA_GAP_LEAD[lang] + detail | |
| def out_of_scope_message(message: str) -> str: | |
| """Refusal for a benign but out-of-scope request (the `out_of_scope` intent).""" | |
| return _OUT_OF_SCOPE["id" if _is_indonesian(message) else "en"] | |
| def blocked_message(message: str) -> str: | |
| """Refusal for a blocked request (injection / secrets / abuse / content-filter).""" | |
| return _BLOCKED["id" if _is_indonesian(message) else "en"] | |