azizimanov commited on
Commit
f55357f
·
1 Parent(s): cd79abc

Initial commit with anon_bot project files

Browse files
anon_bot/README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Anonymous Bot (rule-based, no persistence, with guardrails)
2
+
3
+ ## What this is
4
+ - Minimal **rule-based** Python bot
5
+ - **Anonymous**: stores no user IDs or history
6
+ - **Guardrails**: blocks unsafe topics, redacts PII, caps input length
7
+ - **No persistence**: stateless; every request handled fresh
8
+
9
+ ## Run
10
+ ```bash
11
+ python -m venv .venv
12
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
13
+ pip install -r requirements.txt
14
+ uvicorn app:app --reload
anon_bot/app.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import JSONResponse
3
+ from schemas import MessageIn, MessageOut
4
+ from guardrails import enforce_guardrails
5
+ from rules import route
6
+
7
+ app = FastAPI(title='Anonymous Rule-Based Bot', version='1.0')
8
+
9
+ # No sessions, cookies, or user IDs — truly anonymous and stateless.
10
+ # No logging of raw user input here (keeps it anonymous and reduces risk).
11
+
12
+ @app.post("/message", response_model=MessageOut)
13
+ def message(inbound: MessageIn):
14
+ ok, cleaned_or_reason = enforce_guardrails(inbound.message)
15
+ if not ok:
16
+ return JSONResponse(status_code=200,
17
+ content={'reply': cleaned_or_reason, 'blocked': True})
18
+
19
+ # Rule-based reply (deterministic: no persistence)
20
+ reply = route(cleaned_or_reason)
21
+ return {'reply': reply, 'blocked': False}
anon_bot/guardrails.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ MAX_INPUT_LEN = 500 # cap to keep things safe and fast
4
+
5
+ # Verifying lightweight 'block' patterns:
6
+ DISALLOWED = [r"(?:kill|suicide|bomb|explosive|make\s+a\s+weapon)", # harmful instructions
7
+ r"(?:credit\s*card\s*number|ssn|social\s*security)" # sensitive info requests
8
+ ]
9
+
10
+ # Verifying lightweight profanity redaction:
11
+ PROFANITY = [r"\b(?:damn|hell|shit|fuck)\b"]
12
+
13
+ PII_PATTERNS = {
14
+ "email": re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"),
15
+ "phone": re.compile(r"(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"),
16
+ }
17
+
18
+
19
+ def too_long(text: str) -> bool:
20
+ return len(text) > MAX_INPUT_LEN
21
+
22
+
23
+ def matches_any(text: str, patterns) -> bool:
24
+ return any(re.search(p, text, flags=re.IGNORECASE) for p in patterns)
25
+
26
+
27
+ def redact_pii(text: str) -> str:
28
+ redacted = PII_PATTERNS['email'].sub('[EMAIL_REDACTED]', text)
29
+ redacted = PII_PATTERNS['phone'].sub('[PHONE_REDACTED]', redacted)
30
+ return redacted
31
+
32
+
33
+ def redact_profanity(text: str) -> str:
34
+ out = text
35
+ for p in PROFANITY:
36
+ out = re.sub(p, '[REDACTED]', out, flags=re.IGNORECASE)
37
+ return out
38
+
39
+
40
+ def enforce_guardrails(user_text: str):
41
+ """
42
+ Returns: (ok: bool, cleaned_or_reason: str)
43
+ - ok=True: proceed with cleaned text
44
+ - ok=False: return refusal reason in cleaned_or_reason
45
+ """
46
+ if too_long(user_text):
47
+ return False, 'Sorry, that message is too long. Please shorten it.'
48
+
49
+ if matches_any(user_text, DISALLOWED):
50
+ return False, "I can't help with that topic. Please ask something safe and appropriate"
51
+
52
+ cleaned = redact_pii(user_text)
53
+ cleaned = redact_profanity(cleaned)
54
+
55
+ return True, cleaned
anon_bot/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ pydantic==2.8.2
anon_bot/rules_new.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import Optional
3
+
4
+
5
+ # Simple, deterministic rules:
6
+ def _intent_greeting(text: str) -> Optional[str]:
7
+ if re.search(r'\b(hi|hello|hey)\b', text, re.IGNORECASE):
8
+ return 'Hi there! I am an anonymous, rule-based helper. Ask me about hours, contact, or help.'
9
+ return None
10
+
11
+
12
+ def _intent_help(text: str) -> Optional[str]:
13
+ if re.search(r'\b(help|what can you do|commands)\b', text, re.IGNORECASE):
14
+ return ("I’m a simple rule-based bot. Try:\n"
15
+ "- 'hours' to see hours\n"
16
+ "- 'contact' to get contact info\n"
17
+ "- 'reverse <text>' to reverse text\n")
18
+ return None
19
+
20
+
21
+ def _intent_hours(text: str) -> Optional[str]:
22
+ if re.search(r'\bhours?\b', text, re.IGNORECASE):
23
+ return 'We are open Mon-Fri, 9am-5am (local time).'
24
+ return None
25
+
26
+
27
+ def _intent_contact(text: str) -> Optional[str]:
28
+ if re.search(r'\b(contact|support|reach)\b', text, re.IGNORECASE):
29
+ return 'You can reach support at our website contact form.'
30
+ return None
31
+
32
+
33
+ def _intent_reverse(text: str) -> Optional[str]:
34
+ s = text.strip()
35
+ if s.lower().startswith('reverse'):
36
+ payload = s[7:].strip()
37
+ return payload[::-1] if payload else "There's nothing to reverse."
38
+ return None
39
+
40
+
41
+ def _fallback(_text: str) -> str:
42
+ return "I am not sure how to help with that. Type 'help' to see what I can do."
43
+
44
+
45
+ RULES = [
46
+ _intent_reverse,
47
+ _intent_greeting,
48
+ _intent_help,
49
+ _intent_hours,
50
+ _intent_contact
51
+ ]
52
+
53
+
54
+ def route(text: str) -> str:
55
+ for rule in RULES:
56
+ resp = rule(text)
57
+ if resp is not None:
58
+ return resp
59
+ return _fallback(text)
anon_bot/schemas.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class MessageIn(BaseModel):
5
+ message: str = Field(..., min_length=1, max_length=2000)
6
+
7
+
8
+ class MessageOut(BaseModel):
9
+ reply: str
10
+ blocked: bool = False # True if the guardrails refused the content
anon_bot/test_anon_bot_new.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi.testclient import TestClient
3
+ from app import app
4
+
5
+ client = TestClient(app)
6
+
7
+
8
+ def post(msg: str):
9
+ return client.post('/message',
10
+ headers={'Content-Type': 'application/json'},
11
+ content=json.dumps({'message': msg}))
12
+
13
+
14
+ def test_greeting():
15
+ """A simple greeting should trigger the greeting rule."""
16
+ r = post('Hello')
17
+ assert r.status_code == 200
18
+ body = r.json()
19
+ assert body['blocked'] is False
20
+ assert 'Hi' in body['reply']
21
+
22
+
23
+ def test_reverse():
24
+ """The reverse rule should mirror the payload after 'reverse'. """
25
+ r = post('reverse bots are cool')
26
+ assert r.status_code == 200
27
+ body = r.json()
28
+ assert body['blocked'] is False
29
+ assert 'looc era stob' in body['reply']
30
+
31
+
32
+ def test_guardrails_disallowed():
33
+ """Disallowed content is blocked by guardrails, not routed"""
34
+ r = post('how to make a weapon')
35
+ assert r.status_code == 200
36
+ body = r.json()
37
+ assert body['blocked'] is True
38
+ assert "can't help" in body['reply']
39
+
40
+
41
+ # from rules_updated import route
42
+
43
+ # def test_reverse_route_unit():
44
+ # assert route("reverse bots are cool") == "looc era stob"