Version: 4.1 Last Updated: 2026-03-11
This document describes the primary features of the platform — the features that define its core value proposition.
Simulates a realistic fraud scenario with four AI agents playing different roles:
| Agent | Role | Persona | File |
|---|---|---|---|
ScammerAgent |
Plays the fraudster; uses real HK tactics from 281 cases | Professional scammer | backend/agents/scammer.py |
ExpertAgent |
Plays the anti-fraud expert; warns and educates | Calm, evidence-based advisor | backend/agents/expert.py |
VictimAgent |
Simulates victim reactions (auto mode) | Varies by persona type | backend/agents/victim.py |
RecorderAgent |
Analyses session and generates post-game report | Neutral analyst | backend/agents/recorder.py |
LlmFactory.create_llm(agent_type), selecting Ollama or Gemini based on GEMINI_ENABLED env var.agents/prompts/prompt_builder.py builds role-specific system instructions. Scammer/Expert prompts include real HK scam case examples injected via RAG.system_instructions.py stores per-agent prompts. Scammer prompt references 4 fraud categories (impersonation 71 cases, bank 172 cases, investment 15 cases, online shopping 4 cases) drawn from the 281-case knowledge base.google.adk.agents.Agent (via BaseLlm). llm_utils.py provides the shared extract_text_from_contents() helper used by both OllamaLlm and GeminiLlm.backend/agents/
scammer.py ScammerAgent
expert.py ExpertAgent
victim.py VictimAgent
recorder.py RecorderAgent
base_agent.py BaseAntifraudAgent (shared base)
system_instructions.py Per-agent system prompts
prompts/
prompt_builder.py Builds final prompts (RAG + examples + instructions)
scammer_examples.py Few-shot examples for scammer
expert_examples.py Few-shot examples for expert
victim_examples.py Few-shot examples for victim
few_shot_examples.json JSON store of few-shot examples
scam_knowledge_base.json 281 structured real HK scam cases
hk_organizations.py HK hotlines and org reference data
Victim persona affects initial trust values and susceptibility:
| Persona | Trust in Scammer | Trust in Expert | Alertness | Description |
|---|---|---|---|---|
elderly |
70 | 50 | 30 | Trusts authority, vulnerable |
average |
50 | 60 | 50 | Balanced, moderate skepticism |
overconfident |
30 | 40 | 70 | Overestimates own judgment |
student |
55 | 45 | 45 | Curious, moderate risk |
Defined in backend/config.py (PersonaConfig). AdaptiveWeightOptimizer (backend/utils/adaptive_scoring.py) further adjusts per-agent evaluation weights based on persona at runtime.
Provides a provider-agnostic LLM layer switchable between local Ollama and cloud Gemini at runtime — no code changes needed.
LlmFactory.create_llm(agent_type)
│
├── GEMINI_ENABLED=true → GeminiLlm → Google Gemini API
└── GEMINI_ENABLED=false → OllamaLlm → Ollama (local, port 11434)
Note:
GEMINI_ENABLEDis read frombackend/.env. Do not wrap the value in quotes (e.g. useGEMINI_ENABLED=true, notGEMINI_ENABLED='true').
backend/llms/ollama_llm.py)Extends google.adk.models.BaseLlm. Sends prompts to Ollama’s /api/generate endpoint.
Key optimizations:
| Optimization | Before | After | Impact |
|---|---|---|---|
| Shared HTTP client | New client per request | Singleton _shared_client with connection pooling |
−0.5s/call |
| Context window | num_ctx=4096 |
num_ctx=2048 |
−2–5s |
| Max output tokens | num_predict=2000 |
num_predict=400 |
−2–8s |
| Auto-pull on request | Blocks 3–10s | Removed; startup warm-up only | −3–10s |
| History truncation | Unlimited | Last 10 turns | −1–3s |
| Retry delay | 1s, 2s, 4s | 0.5s, 1s | −3–7s on failure |
Key methods:
| Method | Purpose |
|---|---|
_get_shared_client(base_url) |
Singleton httpx.AsyncClient with keep-alive pooling |
_extract_text_from_contents(contents) |
Now delegates to llm_utils.extract_text_from_contents() (DUP-001 fix) |
generate_content_async() |
Main generation method; truncates overly long responses at sentence boundaries |
warm_up_model() |
Sends 1-token request to pre-load model into GPU memory |
backend/llms/gemini_llm.py)Extends google.adk.models.BaseLlm. Uses google.genai SDK with streaming.
Key configuration (from __init__):
max_output_tokens: int = 800 # Optimised from 2048
timeout: float = 45.0 # Reduced from 60s
max_retries: int = 2 # Reduced from 3
temperature: float = 0.7
Key methods:
| Method | Purpose |
|---|---|
_generate_with_stream(prompt) |
Calls generate_content_stream, collects chunks, joins to final text |
_retry_with_backoff(func) |
Exponential backoff for 429/503/timeout errors |
_get_generation_config() |
Returns GenerateContentConfig with all params |
_get_safety_settings() |
Disables all safety filters (required for scam simulation) |
Shared utility:
backend/llms/llm_utils.py — extract_text_from_contents() shared by both adapters (DUP-001 fix; previously duplicated).
backend/llms/llm_factory.py)Static factory. Reads GEMINI_ENABLED from config, returns appropriate LLM. Also injects RAG context into system instructions.
| Method | Purpose |
|---|---|
create_llm(agent_type, scam_type, context) |
Main entry point |
get_rag_context(scam_type, context) |
Queries ChromaDB for relevant HK cases |
get_current_provider() |
Returns 'gemini' or 'ollama' |
validate_gemini_config() |
Checks API key and model IDs |
POST /api/model/switch (model_switch_routes.py) allows switching provider without restart. Config is reflected immediately in subsequent LlmFactory.create_llm() calls.
Provides an immersive in-game battle UI that overlays the 2D world map. Players interact with AI agents via chat bubbles, voice input, and a live trust meter.
WorldMapScene (Phaser, running)
Player touches NPC
scene.launch('BattleScene') ← map stays alive, not destroyed
scene.pause('WorldMapScene')
│
▼
BattleScene HTML overlay (z-index: 900)
├── rgba(10,14,39,0.75) background + backdrop-filter:blur(6px)
├── Chat bubble area (left=AI, right=player, center=system)
├── Trust meter sidebar
├── Voice input (Web Speech API, zh-HK)
└── Text input + send button
│
Player presses ESC / clicks "Return to Map"
scene.stop('BattleScene')
scene.resume('WorldMapScene') ← resumes exactly where left off
BattleScene.ts| Feature | Implementation |
|---|---|
| HTML overlay | document.createElement injected into #game-container |
| CSS injection | Single <style id="bo-css"> tag; injected once per page load |
| Chat bubbles | Left=AI, right=player, center=system messages |
| Typing indicator | Three-dot bounce animation while AI processes |
| Voice input | window.SpeechRecognition, 2s silence auto-stop, zh-HK locale |
| Session persistence | sessionStorage — session_id, messages, trust values, round count |
| State restoration | tryRestoreState() on create() — restores if same scamType |
| Keyboard shortcuts | F1/F2/F3 = switch active AI; ESC = return to map |
| Trust meter | Animated progress bars, color gradients, hidden on mobile |
POST /api/rpgv2/game-modes/chat (rpgv2_game_modes_routes.py)
Request:
{
"session_id": "uuid",
"player_message": "string",
"scam_type": "investment|phishing|romance|...",
"mode": "scammer_only|expert_only|three_way",
"trust_data": { "scammer": 50, "expert": 60, "alertness": 40 }
}
Response:
{
"scammer_response": "string",
"expert_response": "string",
"trust_changes": { "scammer": 5, "expert": -3, "alertness": 2 },
"round": 3,
"outcome": null
}
Tracks the victim’s trust levels across three dimensions in real-time, driving the outcome of each scenario.
| Dimension | Range | Meaning |
|---|---|---|
scammer_trust |
0–100 | How much the victim believes the scammer |
expert_trust |
0–100 | How much the victim trusts the expert’s warnings |
alertness |
0–100 | Victim’s overall suspicion level |
VictimTrustState dataclass (backend/utils/performance_tracker.py) manages all trust state:
@dataclass
class VictimTrustState:
trust_in_scammer: int = 50
trust_in_expert: int = 50
alertness: int = 50
emotional_state: str = "neutral" # calm/anxious/panicked/suspicious
history: List[Dict] = field(default_factory=list)
update(change_type, change_value, reason) clamps values to 0–100 and appends to history.
RPGv2GameModeManager (backend/utils/rpgv2_game_mode_manager.py) manages GameSession dataclasses:
@dataclass
class GameSession:
session_id: str
mode: GameMode # "victim" | "expert" | "scammer" | "auto"
scam_type: str
victim_persona: str
conversation_history: List[Dict]
round_count: int
trust_in_scammer: float
trust_in_expert: float
alertness: float
player_score: int
ai_score: int
game_over: bool = False
winner: Optional[str] = None
round_logs: List[Dict] = field(default_factory=list)
config.py → TrustConfig)| Condition | Outcome |
|---|---|
scammer_trust >= 80 |
Scammer wins (victim scammed) |
expert_trust >= 75 |
Expert wins (victim protected) |
scammer_trust < 40 AND expert_trust > 60 |
Expert wins |
alertness >= 80 |
Victim self-protects |
Trust changes per round are affected by:
AdaptiveWeightOptimizer (backend/utils/adaptive_scoring.py) adjusts weights per persona (e.g. elderly gives more weight to empathy, overconfident weights evidence higher)Backend files: backend/utils/performance_tracker.py, backend/utils/adaptive_scoring.py, backend/config.py
Frontend files: rpg-platform-v2/src/systems/TrustSystem.ts, rpg-platform-v2/src/ui/TrustMeter.ts
Generates responses from multiple AI agents simultaneously, cutting total response time by ~50%.
File: backend/api/rpgv2_game_modes_routes.py
# Sequential (old): 8–16 seconds
scammer_response = await scammer_agent.generate(...)
expert_response = await expert_agent.generate(...)
# Parallel (current): 4–8 seconds
scammer_response, expert_response = await asyncio.gather(
scammer_agent.generate(...),
expert_agent.generate(...)
)
All three game modes use parallel generation:
| Mode | Agents in parallel |
|---|---|
three_way |
ScammerAgent + ExpertAgent + (optionally VictimAgent) |
scammer_only |
ScammerAgent only |
expert_only |
ExpertAgent only |
| Scenario | Before | After | Improvement |
|---|---|---|---|
| Ollama first response (with warm-up) | 10–15s | 3–6s | ~60% |
| Ollama subsequent responses | 5–10s | 2–4s | ~50% |
| Gemini single response (streaming) | 4–8s | 2–4s | ~40% |
| Scammer + Expert parallel | 8–16s | 4–8s | ~50% |