# Phase 3.1: Demo Chatbot - Research

**Researched:** 2026-02-26
**Domain:** Python terminal REPL chatbot + google-genai SDK + meshulash-guard SDK integration
**Confidence:** HIGH

## Summary

This phase builds a single Python script — a terminal REPL chatbot powered by Gemini that routes all input and output through meshulash-guard scanners. The script is both a personal testing tool and a reference implementation showing real SDK usage patterns.

The standard stack is minimal and already determined: `google-genai` (the current, GA-status replacement for the deprecated `google-generativeai`) for the Gemini client, and `meshulash-guard` (installed from the local `.venv/`) for scanning. No framework, no UI, no streaming required. The chat loop uses `client.chats.create()` to maintain conversation history automatically across turns.

The core design challenge is that the demo must thread the scan-before-send / scan-after-receive / deanonymize pattern clearly through the Gemini chat loop, while preset selection at launch configures which scanners are active. The deanonymize workflow (scan input with REPLACE → redact → send to Gemini → deanonymize response) must be explicitly wired in, because `guard.scan_output()` and `guard.deanonymize()` do not fire automatically.

**Primary recommendation:** One script file at `demo/chatbot.py`, installed into the `.venv` via `pip install -e .` (already done). Use `google-genai`'s `client.chats.create()` with `send_message()` for multi-turn state. Preset selection via numbered menu at startup. Output uses `print()` with structured human-readable labels — no logging framework needed.

## Standard Stack

### Core

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `google-genai` | `>=1.0` (current: ~1.62) | Gemini API client — chat sessions, multi-turn conversation | GA since May 2025, replaces deprecated `google-generativeai`; official recommended SDK |
| `meshulash-guard` | `0.1.0` (local) | All scanning — scan_input, scan_output, deanonymize | Already built and installed in `.venv` |
| `os` (stdlib) | stdlib | Read `GEMINI_API_KEY`, `MESHULASH_API_KEY`, `MESHULASH_SERVER_URL` env vars | No extra dependency; `os.environ.get()` with early validation is idiomatic |

### Supporting

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `sys` (stdlib) | stdlib | `sys.exit()` for clean termination | Use when env vars are missing at startup |

### Alternatives Considered

| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| `google-genai` | `google-generativeai` (legacy) | `google-generativeai` is deprecated as of Gemini 2.0 / May 2025 — do NOT use |
| `os.environ` | `python-dotenv` | `dotenv` adds a dependency; env vars work fine for a personal testing tool; no `.env` file needed |
| Single script `demo/chatbot.py` | Full package structure | Overkill for a single-purpose demo; one file is simpler and easier to read as reference |

### Installation

```bash
# google-genai goes into the project .venv alongside meshulash-guard
/Users/shalev/Desktop/meshulash_guard/.venv/bin/pip install google-genai
```

The demo script runs inside the same `.venv` that has `meshulash-guard` installed via `pip install -e .`.

## Architecture Patterns

### Recommended Project Structure

```
meshulash_guard/
├── src/meshulash_guard/     # SDK (already built)
├── demo/
│   └── chatbot.py           # Single demo script (this phase)
└── .venv/                   # Existing virtualenv
```

The `demo/` directory sits at the project root, parallel to `src/`. The script is not a package — it is run directly: `.venv/bin/python demo/chatbot.py`.

### Pattern 1: Preset-Based Scanner Configuration

**What:** Hardcoded named presets selected at startup via a numbered menu. Each preset is a dict or named constant mapping a display name to a list of scanner instances.

**When to use:** Always — presets are the locked decision for this phase. Do not build runtime scanner configuration.

**Example:**

```python
from meshulash_guard import (
    Guard, Action, Condition,
    PIIScanner, PIILabel,
    ToxicityScanner, ToxicityLabel,
    TopicScanner, TopicLabel,
    CyberScanner, CyberLabel,
)

PRESETS = {
    "1": {
        "name": "PII Protection",
        "description": "Replaces PII (names, emails, phones, addresses) with placeholders",
        "scanners": [
            PIIScanner(labels=[PIILabel.PII], action=Action.REPLACE),
        ],
    },
    "2": {
        "name": "PII Protection + Overrides",
        "description": "Replace most PII, but LOG secrets rather than replace",
        "scanners": [
            PIIScanner(
                labels=[PIILabel.PII, PIILabel.SECRETS],
                action=Action.REPLACE,
                overrides={PIILabel.SECRETS: Action.LOG},
            ),
        ],
    },
    "3": {
        "name": "Content Moderation",
        "description": "Blocks toxic content and restricts sensitive topics",
        "scanners": [
            ToxicityScanner(labels=[ToxicityLabel.TOXICITY, ToxicityLabel.HATE_SPEECH], action=Action.BLOCK),
            TopicScanner(labels=[TopicLabel.SEXUAL_EXPLICIT, TopicLabel.IDENTITY_ATTACK], action=Action.BLOCK),
        ],
    },
    "4": {
        "name": "Cyber Threat Detection",
        "description": "Blocks prompts containing cyber attack content",
        "scanners": [
            CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK),
        ],
    },
    "5": {
        "name": "Full Guard",
        "description": "All scanners active: PII replaced, toxic/cyber blocked",
        "scanners": [
            PIIScanner(labels=[PIILabel.ALL], action=Action.REPLACE),
            ToxicityScanner(labels=[ToxicityLabel.ALL], action=Action.BLOCK),
            CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK),
        ],
    },
}
```

**Key design notes:**
- Preset 1 and 2 exercise PII scanner with bundles and with overrides (both `to_guardline_spec` and `to_guardline_specs` paths)
- Preset 3 exercises TC scanners with BLOCK action
- Preset 4 exercises CyberScanner with ALL label expansion
- Preset 5 combines scanner types — exercises multi-scanner aggregation
- JailbreakScanner omitted from presets (server head "coming soon" — would produce empty results silently; can be included in Full Guard with a comment noting forward-compat stub)

### Pattern 2: The Scan → Gate → LLM → Deanonymize Loop

**What:** The core chatbot loop. Each user turn:
1. `scan_input()` → check status → gate or allow
2. If BLOCK: print block message, skip LLM
3. If REPLACE/LOG/clean: send `result.processed_text` to Gemini
4. Receive Gemini response
5. `scan_output()` → check status → gate or allow (for output scanning)
6. `deanonymize()` on the final output text → print restored text

**When to use:** Every chatbot turn, for every preset.

**Example:**

```python
def chat_turn(guard, chat_session, scanners, user_input):
    # 1. Scan input
    input_result = guard.scan_input(user_input, scanners=scanners)
    _print_scan_result("INPUT", input_result)

    # 2. Gate on block
    if input_result.status == "blocked":
        print("[BLOCKED] Input was blocked. Not forwarding to LLM.")
        return

    # 3. Send (possibly redacted) text to Gemini
    text_to_send = input_result.processed_text
    gemini_response = chat_session.send_message(text_to_send)
    raw_llm_text = gemini_response.text

    # 4. Scan output
    output_result = guard.scan_output(raw_llm_text, scanners=scanners)
    _print_scan_result("OUTPUT", output_result)

    if output_result.status == "blocked":
        print("[BLOCKED] LLM response blocked by output scanner.")
        return

    # 5. Deanonymize (no-op if no PII was replaced)
    final_text = guard.deanonymize(output_result.processed_text)

    print(f"\nAssistant: {final_text}\n")
```

### Pattern 3: Output Formatting (Developer-Realistic)

**What:** Print scan results in a way that mirrors what a real developer would log — not a raw JSON dump, not silent. Show what triggered, what action was taken, and what the score was.

**When to use:** Every scan call. The output format is part of the reference implementation value.

**Example:**

```python
def _print_scan_result(direction: str, result: ScanResult) -> None:
    """Print scan result in a developer-realistic format."""
    status_tag = f"[{result.status.upper()}]"
    print(f"\n-- {direction} SCAN {status_tag} --")
    if result.risk_categories:
        print(f"   Risk categories: {', '.join(result.risk_categories)}")
    for scanner_id, sr in result.scanners.items():
        if sr.triggered:
            detections_summary = ", ".join(
                f"{d.label}({d.action})" for d in sr.detections
            )
            print(f"   {scanner_id}: triggered | score={sr.score:.2f} | {detections_summary}")
    if result.placeholders:
        print(f"   Placeholders created: {len(result.placeholders)}")
```

**Key output principles from CONTEXT.md:**
- Show what triggered and what action was taken — clearly
- Not "overly verbose debug dumps" — only print scanner details when triggered
- Not silent — a clean scan should still print `[CLEAN]` or similar so the developer knows scanning ran

### Pattern 4: Startup Env Validation

**What:** Validate all required environment variables at startup. Exit with a clear message if any are missing. This is what a real developer integrating the SDK would do.

**When to use:** Always, before constructing any client objects.

**Example:**

```python
import os
import sys

def _require_env(name: str) -> str:
    val = os.environ.get(name)
    if not val:
        print(f"Error: {name} environment variable is not set.", file=sys.stderr)
        sys.exit(1)
    return val

GEMINI_API_KEY = _require_env("GEMINI_API_KEY")
MESHULASH_API_KEY = _require_env("MESHULASH_API_KEY")
MESHULASH_SERVER_URL = _require_env("MESHULASH_SERVER_URL")
```

### Pattern 5: REPL Loop with Graceful Exit

**What:** Standard `while True` input loop catching `KeyboardInterrupt` (Ctrl+C) and `EOFError` (Ctrl+D) for clean exit.

**When to use:** Always for terminal REPL scripts.

**Example:**

```python
while True:
    try:
        user_input = input("You: ").strip()
    except (KeyboardInterrupt, EOFError):
        print("\nGoodbye.")
        break
    if not user_input:
        continue
    # ... process turn
```

### Pattern 6: Google Genai Chat Session (Multi-Turn)

**What:** `client.chats.create()` returns a `Chat` object that automatically accumulates conversation history. Each `chat.send_message()` call sends the full history + new message to Gemini.

**Source:** [googleapis.github.io/python-genai](https://googleapis.github.io/python-genai/) — verified from official SDK docs.

**Example:**

```python
from google import genai
from google.genai import types

client = genai.Client(api_key=GEMINI_API_KEY)

chat = client.chats.create(
    model="gemini-2.5-flash",
    config=types.GenerateContentConfig(
        system_instruction="You are a helpful assistant.",
    ),
)

# Each call maintains conversation context automatically
response = chat.send_message("Hello, my name is Alice.")
print(response.text)

response = chat.send_message("What is my name?")
print(response.text)  # Remembers "Alice"
```

### Anti-Patterns to Avoid

- **Streaming responses:** Do not use `send_message_stream()`. Non-streaming is simpler, sufficient for a testing tool, and makes the scan-before-display flow cleaner.
- **Async API:** Do not use `client.aio.chats`. Synchronous is appropriate for a single-threaded REPL.
- **Scanning inside the loop without gating:** Always check `result.status == "blocked"` before forwarding to Gemini. Skipping the gate check defeats the purpose of the demo.
- **Printing raw `result.model_dump()`:** Verbose JSON dumps obscure what actually matters — use structured print statements.
- **Reconstructing conversation history manually:** Do not build a `messages` list. `client.chats.create()` handles history automatically.
- **Calling `guard.clear_cache()` between turns:** The vault must persist across turns for deanonymize to work when PII was replaced in a previous turn's input. Only clear at the start of a new conversation (if implementing a "reset" command).

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Multi-turn conversation history | Manual messages list accumulation | `client.chats.create()` + `send_message()` | SDK manages history; avoids off-by-one errors in context window management |
| Env var validation | Try/except around os.environ[key] | `os.environ.get(name)` + explicit None check with `sys.exit(1)` | Simple, readable, idiomatic |
| Placeholder-to-original mapping | Custom string replacement logic | `guard.deanonymize()` | Already implemented with longest-first sort to prevent substring collisions |
| Scanner preset management | Dynamic config parsing | Hardcoded dict of preset objects | Locked decision; simpler, more readable as reference code |

**Key insight:** The meshulash-guard SDK already does the hard work — bundle expansion, multi-spec splitting, vault accumulation. The demo's job is to wire it correctly, not rebuild it.

## Common Pitfalls

### Pitfall 1: Sending Original Text to Gemini When Status is "secured"

**What goes wrong:** Developer sends `user_input` to Gemini instead of `result.processed_text`, bypassing the redaction.
**Why it happens:** Easy to use the original variable by mistake; "secured" status is less obvious than "blocked".
**How to avoid:** Always use `input_result.processed_text` as the text sent to Gemini, never the raw `user_input`.
**Warning signs:** PII appears in Gemini prompts when PII scanner with REPLACE is active.

### Pitfall 2: Deanonymize Called on Wrong Text

**What goes wrong:** Calling `guard.deanonymize(gemini_response.text)` instead of `guard.deanonymize(output_result.processed_text)`, skipping the output scan entirely.
**Why it happens:** The response chain has three text objects: raw Gemini text, output-scanned text, deanonymized text.
**How to avoid:** The chain must be: `gemini_response.text` → `guard.scan_output()` → `output_result.processed_text` → `guard.deanonymize()` → `final_text`.

### Pitfall 3: Vault Not Accumulating Across Turns

**What goes wrong:** `guard.clear_cache()` called inside the loop, resetting placeholders before deanonymize can use them.
**Why it happens:** Confusion about when to clear.
**How to avoid:** Never call `clear_cache()` inside the turn loop. Only call it on explicit user request (e.g., a `/reset` command) or at session start.

### Pitfall 4: BLOCK Status Not Gated Before Sending to Gemini

**What goes wrong:** Toxic or blocked input still gets forwarded to Gemini despite the scan returning "blocked".
**Why it happens:** Developer checks status for display but forgets to `return` or `continue` before the Gemini call.
**How to avoid:** Always `return` / `continue` from the turn handler immediately when `result.status == "blocked"`.

### Pitfall 5: JailbreakScanner in "Full Guard" Preset Causes Confusion

**What goes wrong:** Including `JailbreakScanner` in Full Guard preset produces empty results silently — developer thinks it's not working.
**Why it happens:** The server jailbreak TC head is "coming soon" (per STATE.md). The scanner is a forward-compatible stub.
**How to avoid:** Either omit JailbreakScanner from Full Guard preset, or include it with a prominent comment explaining the forward-compat behavior. Including it with a comment is the better reference implementation choice — shows the developer how to add future scanners.

### Pitfall 6: google-genai Model Name

**What goes wrong:** Using an outdated or non-existent model name (e.g., `gemini-pro`, `gemini-1.5-flash`, `gemini-2.0-flash-001`).
**Why it happens:** Training data is stale; model names change frequently.
**How to avoid:** Use `gemini-2.5-flash` — confirmed available and recommended as of February 2026. Gemini 2.0 Flash is scheduled for retirement June 2026. Gemini 2.5 Flash is the current "best price-performance" model.

### Pitfall 7: PIILabel Enum __str__ in Python 3.12+

**What goes wrong:** `str(PIILabel.EMAIL_ADDRESS)` returns `'PIILabel.EMAIL_ADDRESS'` instead of `'EMAIL_ADDRESS'` in Python 3.12+.
**Why it happens:** Python 3.12 changed `(str, Enum).__str__` behavior.
**How to avoid:** This is already fixed in the SDK via `_StrValueMixin`. The demo does not need to work around this — just use the enums normally.

## Code Examples

Verified patterns from official sources and from the existing SDK codebase.

### Full Demo Script Skeleton

```python
#!/usr/bin/env python3
"""meshulash-guard Demo Chatbot

Reference implementation showing how to integrate the SDK into a Gemini-powered app.

Usage:
    export GEMINI_API_KEY="..."
    export MESHULASH_API_KEY="..."
    export MESHULASH_SERVER_URL="https://app.meshulash.ai"
    python demo/chatbot.py
"""

import os
import sys

from google import genai
from google.genai import types

from meshulash_guard import (
    Guard, Action, Condition,
    PIIScanner, PIILabel,
    ToxicityScanner, ToxicityLabel,
    TopicScanner, TopicLabel,
    CyberScanner, CyberLabel,
    JailbreakScanner, JailbreakLabel,
    ScanResult,
    AuthError, ConnectionError, ServerError,
)

# ---------------------------------------------------------------------------
# Environment
# ---------------------------------------------------------------------------

def _require_env(name: str) -> str:
    val = os.environ.get(name)
    if not val:
        print(f"Error: {name} is not set.", file=sys.stderr)
        sys.exit(1)
    return val

GEMINI_API_KEY = _require_env("GEMINI_API_KEY")
MESHULASH_API_KEY = _require_env("MESHULASH_API_KEY")
MESHULASH_SERVER_URL = _require_env("MESHULASH_SERVER_URL")

# ---------------------------------------------------------------------------
# Presets
# ---------------------------------------------------------------------------

PRESETS = { ... }  # See Pattern 1 above

# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------

def _print_scan_result(direction: str, result: ScanResult) -> None:
    ...  # See Pattern 3 above

# ---------------------------------------------------------------------------
# Chat turn
# ---------------------------------------------------------------------------

def chat_turn(guard: Guard, chat_session, scanners: list, user_input: str) -> None:
    ...  # See Pattern 2 above

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
    # Preset selection
    print("meshulash-guard Demo Chatbot\n")
    print("Select a preset:")
    for key, preset in PRESETS.items():
        print(f"  {key}. {preset['name']} — {preset['description']}")

    choice = input("\nPreset number: ").strip()
    if choice not in PRESETS:
        print("Invalid choice. Exiting.")
        sys.exit(1)

    preset = PRESETS[choice]
    scanners = preset["scanners"]
    print(f"\nActive preset: {preset['name']}\n")

    # Initialize Guard
    guard = Guard(
        api_key=MESHULASH_API_KEY,
        server_url=MESHULASH_SERVER_URL,
    )

    # Initialize Gemini chat
    gemini_client = genai.Client(api_key=GEMINI_API_KEY)
    chat_session = gemini_client.chats.create(
        model="gemini-2.5-flash",
        config=types.GenerateContentConfig(
            system_instruction="You are a helpful assistant.",
        ),
    )

    print("Chat started. Type your message. Press Ctrl+C or Ctrl+D to exit.\n")

    while True:
        try:
            user_input = input("You: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\nGoodbye.")
            break
        if not user_input:
            continue
        try:
            chat_turn(guard, chat_session, scanners, user_input)
        except (AuthError, ConnectionError, ServerError) as e:
            print(f"[ERROR] SDK error: {e}")

if __name__ == "__main__":
    main()
```

### google-genai: Chat Session with System Instruction

```python
# Source: googleapis.github.io/python-genai/ (official SDK docs, verified 2026-02-26)
from google import genai
from google.genai import types

client = genai.Client(api_key=GEMINI_API_KEY)

chat = client.chats.create(
    model="gemini-2.5-flash",
    config=types.GenerateContentConfig(
        system_instruction="You are a helpful assistant.",
    ),
)

response = chat.send_message("Hello!")
print(response.text)
```

### Deanonymize Workflow

```python
# Source: meshulash_guard/guard.py (existing SDK implementation)
# Input scan with REPLACE — creates placeholders in vault
input_result = guard.scan_input(user_input, scanners=scanners)
# Send redacted text to Gemini
response = chat.send_message(input_result.processed_text)
# Output scan
output_result = guard.scan_output(response.text, scanners=scanners)
# Restore originals (no-op if no PII was replaced)
final_text = guard.deanonymize(output_result.processed_text)
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `google-generativeai` package | `google-genai` package | May 2025 (GA) | Must use `google-genai` — `google-generativeai` is deprecated |
| `genai.GenerativeModel().start_chat()` | `client.chats.create()` | May 2025 | New API, different import and call pattern |
| `model.generate_content()` | `client.models.generate_content()` | May 2025 | Client-based, not model-based |

**Deprecated/outdated:**
- `google-generativeai`: Replaced by `google-genai`. Do NOT use `import google.generativeai as genai`.
- `gemini-2.0-flash-001` model: Scheduled for retirement June 2026. Use `gemini-2.5-flash`.
- `genai.configure(api_key=...)`: Old SDK pattern. New SDK uses `genai.Client(api_key=...)`.

## Open Questions

1. **Guard constructor signature for tenant_id**
   - What we know: `Guard.__init__` takes `api_key`, `tenant_id`, `server_url`, `timeout`. The CONTEXT.md says env vars are `MESHULASH_API_KEY` and `MESHULASH_SERVER_URL`.
   - What's unclear: Is there a `MESHULASH_TENANT_ID` env var needed? The Guard constructor requires `tenant_id` as a positional argument.
   - Recommendation: Add `MESHULASH_TENANT_ID` as a third required env var in the demo, mirroring how `Guard` is constructed in the SDK's own tests. The demo should exercise the real Guard constructor signature.

2. **JailbreakScanner in Full Guard preset**
   - What we know: JailbreakScanner is a forward-compat stub; server head "coming soon".
   - What's unclear: Does including it cause any error, or just return empty results?
   - Recommendation: Include it in Full Guard with a comment. Based on the stub implementation, it should return empty scanner results without error. This makes the demo forward-compatible.

3. **Output scan scanner list**
   - What we know: `scan_output(text, scanners)` accepts the same scanner list as `scan_input`.
   - What's unclear: Should the demo use the same scanners for output as input, or a subset? For PII protection, scanning output with REPLACE is reasonable (Gemini might echo back PII).
   - Recommendation: Use the same scanners list for both input and output scanning. This is the simplest and most natural pattern for a reference implementation.

## Sources

### Primary (HIGH confidence)

- [googleapis.github.io/python-genai/](https://googleapis.github.io/python-genai/) — Chat session API, `client.chats.create()`, `send_message()`, system instruction config pattern
- [ai.google.dev/gemini-api/docs/text-generation](https://ai.google.dev/gemini-api/docs/text-generation) — Multi-turn conversation pattern, conversation history
- [ai.google.dev/gemini-api/docs/libraries](https://ai.google.dev/gemini-api/docs/libraries) — `google-genai` as the current recommended SDK (GA since May 2025)
- `src/meshulash_guard/guard.py` — Guard constructor, `scan_input`, `scan_output`, `deanonymize`, `clear_cache` signatures
- `src/meshulash_guard/models.py` — `ScanResult`, `ScannerResult`, `Detection` field names
- `src/meshulash_guard/scanners/pii.py` — PIIScanner, PIILabel, bundle constants, override workflow
- `src/meshulash_guard/scanners/topic.py` — TopicScanner, TopicLabel
- `src/meshulash_guard/scanners/toxicity.py` — ToxicityScanner, ToxicityLabel
- `src/meshulash_guard/scanners/cyber.py` — CyberScanner, CyberLabel
- `src/meshulash_guard/scanners/jailbreak.py` — JailbreakScanner forward-compat stub

### Secondary (MEDIUM confidence)

- [ai.google.dev/gemini-api/docs/models](https://ai.google.dev/gemini-api/docs/models) — `gemini-2.5-flash` as recommended current model (fetched 2026-02-26)
- WebSearch: `google-genai` current version ~1.62, GA status May 2025, Python 3.9+ support

### Tertiary (LOW confidence)

- WebSearch: Gemini 2.0 Flash retirement scheduled June 2026 (from Galaxy.ai model specs page — treat as directional, verify before planning long-lived integrations)

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — google-genai GA status confirmed from official docs; meshulash-guard built from source
- Architecture: HIGH — SDK API verified from official docs and source code; REPL pattern is trivial standard Python
- Pitfalls: HIGH — sourced from actual SDK source code (vault, deanonymize, scanner stubs) and official docs

**Research date:** 2026-02-26
**Valid until:** 2026-03-28 (google-genai model names evolve quickly; re-verify if >30 days pass)
