Processing Pipeline¶
This document details the step-by-step processing pipeline from incoming HTTP request to outbound response. The pipeline is split between guvi/handler.py (top-level orchestration, session management, callback dispatch) and engine/orchestrator.py (LLM classification, persona response generation, strategy self-correction).
Sequence Diagram¶
sequenceDiagram
participant GUVI as GUVI Evaluator
participant CF as Cloud Function<br/>guvi_honeypot
participant AUTH as validate_api_key()
participant RL as rate_limiter<br/>check_rate_limit()
participant FS as Firestore<br/>honeypot_sessions
participant EXT as Extractors<br/>regex + keywords
participant EI as Evidence Index<br/>evidence_index
participant ORCH as Orchestrator
participant GEMINI as Gemini Flash
participant CB as Callback Service
GUVI->>+CF: POST /guvi_honeypot<br/>{sessionId, message, history, metadata}
CF->>AUTH: Validate x-api-key header
AUTH-->>CF: OK / 401
CF->>RL: check_rate_limit(sessionId)
RL->>FS: Transactional counter read/write
RL-->>CF: (allowed, reason)
Note over CF: If rate limited, return stalling reply
CF->>FS: get_session(sessionId)
FS-->>CF: SessionState or None
Note over CF: Create new session if None
CF->>EXT: extract_all_evidence(all scammer messages)
EXT-->>CF: evidence_dict {upiIds, bankAccounts, ...}
CF->>EXT: extract_suspicious_keywords(all scammer messages)
EXT-->>CF: keywords[]
CF->>EI: find_matching_evidence(evidence_dict)
EI->>FS: Query evidence_index collection
EI-->>CF: cross_session_match<br/>{is_known_scammer, matches, known_scam_types}
CF->>+ORCH: process(session, message, history, metadata,<br/>cross_session_match, context, pre_extracted_evidence)
Note over ORCH: Step 0: Inject quality directives into PipelineContext
ORCH->>+GEMINI: classify_scam(message, context, language)
GEMINI-->>-ORCH: {classification, confidence, red_flags}
Note over ORCH: Step 1b: Apply cross-session confidence boost
Note over ORCH: Step 2: select_persona(scam_type, language)
Note over ORCH: Step 3: Use pre-extracted evidence (skip redundant extraction)
Note over ORCH: Step 4: determine_strategy_adjustment()
ORCH->>+GEMINI: generate_persona_response(persona_prompt,<br/>scam_type, message, history, strategy, context)
GEMINI-->>-ORCH: persona_response_text
Note over ORCH: Step 5: determine_state() (state machine transition)
ORCH-->>-CF: ProcessingResult<br/>{response, scam_type, confidence, evidence,<br/>persona_used, state, strategy_state}
CF->>CF: merge_evidence_locally(existing, new)
CF->>FS: store_evidence_index(sessionId, merged_evidence)
CF->>FS: batch_update_session(sessionId, {<br/>conversation_history, evidence, message_count,<br/>scam_type, confidence, state, persona, strategy})
Note over CF: Compute scamDetected, agentNotes, duration
alt message_count >= CALLBACK_MIN_TURN (1)
CF->>+CB: send_final_result(session, scam_type, message_count)
CB->>GUVI: POST /updateHoneyPotFinalResult<br/>{sessionId, scamDetected, extractedIntelligence, ...}
GUVI-->>CB: 200 OK
CB-->>-CF: success
end
CF-->>-GUVI: 200 GuviResponse<br/>{reply, scamDetected, extractedIntelligence,<br/>engagementMetrics, agentNotes}
Pipeline Steps in Detail¶
Step 1: Authentication¶
File: guvi/handler.py -- validate_api_key()
- In production (
K_SERVICEenv var set): requiresSCAMSHIELD_API_KEYfrom Secret Manager. - In development: allows all requests if key is not configured.
- Returns
401withGuviErrorResponseon failure.
No constant-time comparison
The current implementation uses == for key comparison. The CLAUDE.md references a hardened constant-time comparison fix (commit ba3bc09), but the current code uses standard equality. For a hackathon context this is acceptable; production deployments should use hmac.compare_digest().
Step 2: Rate Limiting¶
File: utils/rate_limiter.py -- check_rate_limit()
Uses a Firestore transactional counter per session with two limits:
| Limit | Value | Scope |
|---|---|---|
MAX_MESSAGES_PER_SESSION |
100 | Lifetime of a session |
MAX_MESSAGES_PER_MINUTE |
10 | Sliding 60-second window |
The minute window is tracked via minute_key = int(now / 60). When the key changes, the per-minute counter resets. If Firestore is unavailable, requests are allowed (fail-open for availability).
When rate-limited, the handler returns a valid GuviResponse with a stalling reply in Hinglish:
"Ek minute ruko beta, bahut zyada messages aa rahe hain."
Step 3: Session Management¶
File: guvi/handler.py -- get_or_create_session()
Sessions are loaded from Firestore's honeypot_sessions collection keyed by sessionId. If no session exists, a new SessionState is created with:
- Default persona:
sharma_uncle - State:
INITIAL - Empty
ExtractedIntelligence - Strategy state:
BUILDING_TRUST
The session is immediately persisted to Firestore so it survives cold starts.
Step 4: Evidence Extraction (Early)¶
File: guvi/handler.py -- _extract_evidence_from_full_conversation()
Evidence is extracted before the orchestrator runs. This serves two purposes:
- The evidence is available for cross-session lookup (Step 5).
- The orchestrator receives pre-extracted evidence, avoiding redundant extraction.
The function concatenates all non-honeypot messages from conversationHistory plus the current message, then runs:
extract_all_evidence()-- 14 regex extractors (see Evidence Extraction)extract_suspicious_keywords()-- 11 weighted keyword categoriestransform_to_guvi_format()-- convertssnake_casekeys tocamelCase
Step 5: Cross-Session Evidence Lookup¶
File: firestore/sessions.py -- find_matching_evidence()
Queries the evidence_index collection for any UPI IDs, bank accounts, phone numbers, or emails that appeared in previous sessions. Returns:
{
"matches": [...], # List of matching evidence items
"total_matching_sessions": 3, # Unique session count
"known_scam_types": ["KYC_BANKING"], # Scam types from prior sessions
"is_known_scammer": True, # True if UPI/bank match found
}
When is_known_scammer is True, the orchestrator applies a confidence boost and injects aggressive extraction tactics into the persona prompt.
Step 6: Pipeline Context Creation¶
File: engine/context.py -- PipelineContext
A dataclass carrying enrichment data through the pipeline. Supports priority-ordered prompt sections:
| Priority | Label | Content |
|---|---|---|
| 10 | edge-case | Bot detection, edge case handling |
| 20 | language | Language/script instructions |
| 25 | quality | Turn-aware quality directives (injected by orchestrator) |
| 30 | engagement | Conversation-stage guidance |
Sections are sorted by priority and concatenated into the final Gemini prompt via get_prompt_sections().
Step 7: Orchestrator Processing¶
File: engine/orchestrator.py -- Orchestrator.process()
The orchestrator executes 6 sub-steps:
7a. Quality Directive Injection¶
Turn-aware quality directives are injected into PipelineContext at priority 25. These guide Gemini to produce responses scoring well on the evaluation rubric:
- Turns 1-3 (Early Engagement): 2 identity verification questions, 1 red flag, show willingness.
- Turns 4-6 (Investigation): 2 investigative questions, 2 red flags, elicit phone/email, demand proof.
- Turns 7+ (Extraction): Demand ID proof, 2+ red flags, final push for phone/email/address/supervisor.
7b. Classification¶
GeminiClient.classify_scam() runs on the current message + last 5 history messages. Returns one of 12 scam types with confidence 0.0-1.0.
Classification is re-run when:
- State is
INITIAL - Scam type is
None,UNKNOWN, orNOT_SCAM
If Gemini fails (error or circuit breaker open), a keyword-based fallback runs using weighted keyword matching.
7c. Cross-Session Confidence Boost¶
For known scammers, confidence is boosted proportionally:
boost = 0.1 * min(match_count, 3) # Max 0.3 for 3+ matches
confidence = min(confidence + boost, 0.95)
7d. Persona Selection¶
Two-tier lookup (see Persona System):
SCAM_PERSONA_MAP[scam_type]-- base mappingLANGUAGE_PERSONA_OVERRIDES[language]-- override unless persona is inLANGUAGE_OVERRIDE_EXEMPT
7e. Strategy Self-Correction¶
determine_strategy_adjustment() analyzes conversation state and transitions between 4 strategy states:
stateDiagram-v2
[*] --> BUILDING_TRUST
BUILDING_TRUST --> EXTRACTING: msg_count >= 3 AND confidence > 0.6
EXTRACTING --> DIRECT_PROBE: 4+ msgs without evidence
EXTRACTING --> PIVOTING: high-value evidence obtained
DIRECT_PROBE --> BUILDING_TRUST: scammer disengaging
PIVOTING --> PIVOTING: continue extracting scammer identity
Each transition produces a tactics_suggestion that is injected into the Gemini prompt.
7f. Persona Response Generation¶
GeminiClient.generate_persona_response() assembles a prompt from:
- Persona system prompt (character profile, speech patterns, strategic behaviors)
- Known scammer context (if repeat offender)
- Strategy context (from self-correction)
- Dynamic pipeline sections (quality directives, language, edge cases)
- Scam type and language
- Conversation history (last 10 messages, sanitized)
- Current scammer message (sanitized)
- 14 critical instructions for response quality
Temperature is set to 0.8 for natural variation. Max output tokens is 1000.
Step 8: Evidence Merging¶
File: guvi/handler.py
New evidence is merged with existing session evidence using merge_evidence_locally(), which performs a set union for each of the 14 evidence fields. This avoids a Firestore read-after-write round-trip.
After merging, high-value evidence (UPI or bank account) boosts confidence to at least 0.85:
Step 9: Persist to Firestore¶
File: guvi/handler.py and firestore/sessions.py -- batch_update_session()
All session updates are written in a single set(merge=True) call:
batch_update_session(session_id, {
"conversation_history": conversation_with_reply,
"extracted_evidence": merged_evidence.model_dump(),
"message_count": actual_message_count,
"scam_type": result.scam_type,
"confidence": final_confidence,
"state": result.state,
"persona": result.persona_used,
"strategy_state": result.strategy_state,
"messages_since_evidence": messages_since_evidence,
"high_value_extracted": has_high_value,
"source": guvi_request.source,
})
Evidence is also indexed in the evidence_index collection for future cross-session lookups.
Step 10: Callback Dispatch¶
File: guvi/handler.py and guvi/callback.py
From turn CALLBACK_MIN_TURN (1) onward, a callback is sent every turn to GUVI's updateHoneyPotFinalResult endpoint. The endpoint uses overwrite semantics, so sending every turn ensures the latest intelligence is always submitted -- even if the evaluator stops at any turn.
The scamDetected flag is computed by _compute_scam_detected():
| Condition | scamDetected |
|---|---|
Classified scam type (not UNKNOWN, not NOT_SCAM) |
True |
NOT_SCAM but UPI/bank evidence found |
True |
UNKNOWN with confidence > 0.5 OR UPI/bank OR 2+ keywords OR phone |
True |
| Otherwise | False |
Step 11: Response Assembly¶
File: guvi/handler.py
The final GuviResponse includes:
GuviResponse(
status="success",
reply=result.response, # Persona's in-character reply
sessionId=guvi_request.sessionId,
scamDetected=scam_detected, # Boolean
scamType=result.scam_type, # e.g. "KYC_BANKING"
confidenceLevel=round(final_confidence, 2),
extractedIntelligence=merged_evidence, # All 14 evidence types
engagementMetrics=EngagementMetrics(...),
totalMessagesExchanged=actual_message_count,
engagementDurationSeconds=round(duration, 1),
agentNotes=agent_notes, # Concise summary
)
Error Handling¶
The pipeline has multiple error boundaries:
- Request parsing errors: Caught in
guvi_honeypot(), returns 200 with stalling reply. - Processing errors: Caught in
process_honeypot_request(), returns 200 with stalling reply. - Gemini failures: Circuit breaker (
pybreaker, 5 failures / 60s window) with keyword fallback for classification and static fallback for response generation. - Firestore failures: In-memory fallback for all session operations.
- Callback failures: Circuit breaker (3 failures / 60s), retry with exponential backoff.
Always 200
Per the evaluation spec, the endpoint always returns HTTP 200 with a valid GuviResponse, even on internal errors. A stalling reply prevents the evaluator from scoring the turn as a total failure.