sangram kumar yerra commited on
Commit
06254f4
Β·
1 Parent(s): 53592ca

Bug fix: Added logic when domain classify confidence is low

Browse files
docs/architecture.md CHANGED
@@ -93,21 +93,33 @@ IDs) that are not redacted by Presidio.
93
 
94
  | Attribute | Value |
95
  |-----------------|----------------------------------------------------|
96
- | Architecture | `distilbert-base-uncased` + linear classification head |
97
- | Task | Multi-class text classification |
98
- | Classes (6) | `ecommerce`, `telecom`, `banking`, `cibil`, `insurance`, `general` |
99
- | Training data | CFPB Consumer Complaint Database (3M+ rows) β€” one-time download from Kaggle. Save as `data/raw/complaints.csv`. |
100
- | Script | `python -m src.classifier.train --cfpb_csv data/raw/complaints.csv --output_dir models/domain_classifier` |
101
- | Input | Redacted complaint text (string, max 512 tokens) |
102
- | Output | `{domain: str, confidence: float, all_probs: dict}` |
103
- | Fine-tune time | ~30 min CPU / ~5 min GPU (T4) |
104
- | Library | HuggingFace `transformers` + `datasets` |
 
 
 
105
 
106
  **Why DistilBERT?**
107
  DistilBERT is 40% smaller and 60% faster than BERT-base with only 3% accuracy
108
  loss. For a project with limited compute, it is the ideal starting point.
109
  The CFPB dataset maps naturally to our 6 classes after label remapping.
110
 
 
 
 
 
 
 
 
 
 
111
  ---
112
 
113
  #### 2.1.2 EvidenceNER
@@ -193,7 +205,13 @@ You are G.U.I.D.E., an expert consumer complaint assistant.
193
  PII has already been redacted locally β€” work with placeholders as-is.
194
 
195
  Rules:
196
- 1. Always classify the domain first using the classify_domain tool.
 
 
 
 
 
 
197
  2. Ask ONE targeted follow-up question at a time if information is missing.
198
  3. If documents are uploaded, always run process_document before drafting.
199
  4. HITL gate: Before calling draft_complaint, present extracted details
 
93
 
94
  | Attribute | Value |
95
  |-----------------|----------------------------------------------------|
96
+ | Architecture | `distilbert-base-uncased` + linear classification head |
97
+ | Task | Multi-class text classification |
98
+ | Classes (6) | `ecommerce`, `telecom`, `banking`, `cibil`, `insurance`, `general` |
99
+ | Training data | CFPB Consumer Complaint Database (3M+ rows) β€” one-time download from Kaggle. Save as `data/raw/complaints.csv`. |
100
+ | Script | `python -m src.classifier.train --cfpb_csv data/raw/complaints.csv --output_dir models/domain_classifier` |
101
+ | Input | Redacted complaint text (string, max 512 tokens) |
102
+ | Output | `DomainResult(domain: str, confidence: float, all_probs: dict, low_confidence: bool)` |
103
+ | Confidence threshold | `0.50` β€” results below this set `low_confidence=True` |
104
+ | Low-confidence path | Agent asks user one clarifying domain question; does not proceed until user confirms |
105
+ | Keyword fallback | Used when no checkpoint exists; always returns `confidence=0.0`, `low_confidence=True` |
106
+ | Fine-tune time | ~30 min CPU / ~5 min GPU (T4) |
107
+ | Library | HuggingFace `transformers` + `datasets` |
108
 
109
  **Why DistilBERT?**
110
  DistilBERT is 40% smaller and 60% faster than BERT-base with only 3% accuracy
111
  loss. For a project with limited compute, it is the ideal starting point.
112
  The CFPB dataset maps naturally to our 6 classes after label remapping.
113
 
114
+ **Low-confidence handling:**
115
+ `general` is the intentional catch-all class β€” the model never returns an error, only a
116
+ domain + probability. However, a low probability on all classes (e.g., the complaint text
117
+ is too short or ambiguous) means the winning domain is unreliable. When `confidence < 0.50`
118
+ the `low_confidence` flag is set and the CMA agent pauses to ask the user one clarifying
119
+ question ("Is this about e-commerce, telecom, banking, credit score, insurance, or other?")
120
+ before continuing. The user's answer overrides the model's suggestion and is stored with
121
+ `domain_source = "user_confirmed"` so later tools know the domain is authoritative.
122
+
123
  ---
124
 
125
  #### 2.1.2 EvidenceNER
 
205
  PII has already been redacted locally β€” work with placeholders as-is.
206
 
207
  Rules:
208
+ 1. Always classify the domain first using classify_domain().
209
+ β€’ If low_confidence=false (β‰₯ 0.50): store domain and proceed.
210
+ β€’ If low_confidence=true (< 0.50 or keyword fallback): ask the user ONE
211
+ clarifying question ("Is this about e-commerce, telecom, banking, credit
212
+ score, insurance, or other?") before continuing. Store domain_source=
213
+ "user_confirmed" when the domain comes from the user.
214
+ β€’ If classify_domain() errors: same clarifying question as above.
215
  2. Ask ONE targeted follow-up question at a time if information is missing.
216
  3. If documents are uploaded, always run process_document before drafting.
217
  4. HITL gate: Before calling draft_complaint, present extracted details
src/agent/prompts.py CHANGED
@@ -30,15 +30,33 @@ step. Never attempt to echo, reconstruct, or guess the original PII behind any p
30
 
31
  ## Operating Rules
32
 
33
- **Rule 1 β€” Classify domain first.**
34
  At the start of every new complaint thread, call classify_domain() with the complaint text as the \
35
  very first action β€” before asking any clarifying questions, before extract_entities(), before \
36
- anything else. Store the returned domain in session memory:
 
 
 
 
 
37
  store_memory(key="domain", value=<domain string>)
38
- Every downstream decision (which questions to ask, which authority to recommend, which draft \
39
- template to use) depends on the domain. If classify_domain() returns an error, fall back to \
40
- asking the user once: "What type of service is involved? (e-commerce, telecom, banking, credit \
41
- score, insurance, or other)". Store the user's answer with the same key.
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  **Rule 2 β€” One follow-up question at a time.**
44
  Collect the following six minimum required fields before drafting:
 
30
 
31
  ## Operating Rules
32
 
33
+ **Rule 1 β€” Classify domain first; verify when uncertain.**
34
  At the start of every new complaint thread, call classify_domain() with the complaint text as the \
35
  very first action β€” before asking any clarifying questions, before extract_entities(), before \
36
+ anything else.
37
+
38
+ Inspect the result's `low_confidence` field:
39
+
40
+ *Case A β€” low_confidence is false (confidence β‰₯ 0.50):*
41
+ The model is confident. Store the domain and proceed:
42
  store_memory(key="domain", value=<domain string>)
43
+ store_memory(key="domain_source", value="model")
44
+
45
+ *Case B β€” low_confidence is true (confidence < 0.50, or keyword fallback was used):*
46
+ The model is uncertain. Do NOT silently use the suggested domain. Instead, ask the user \
47
+ exactly ONE clarifying question as your next response β€” do not call any other tool first:
48
+ "To route your complaint correctly, could you tell me which type of service this is about?
49
+ Choose one: e-commerce / online shopping Β· telecom or internet Β· banking Β·
50
+ credit score or CIBIL Β· insurance Β· other"
51
+ Wait for the user's reply. Map their answer to the closest domain label and store it:
52
+ store_memory(key="domain", value=<confirmed domain string>)
53
+ store_memory(key="domain_source", value="user_confirmed")
54
+
55
+ *Case C β€” classify_domain() returns an error:*
56
+ Ask the same clarifying question as in Case B.
57
+
58
+ Once the domain is confirmed (from any case), continue with extract_entities() and the \
59
+ remaining pipeline. Every downstream decision depends on the domain.
60
 
61
  **Rule 2 β€” One follow-up question at a time.**
62
  Collect the following six minimum required fields before drafting:
src/agent/tools.py CHANGED
@@ -35,7 +35,12 @@ TOOL_DEFINITIONS: list[dict] = [
35
  "description": (
36
  "Classify a consumer complaint into one of six domains: "
37
  "ecommerce, telecom, banking, cibil, insurance, or general. "
38
- "MUST be the very first tool called on every new complaint thread."
 
 
 
 
 
39
  ),
40
  "input_schema": {
41
  "type": "object",
 
35
  "description": (
36
  "Classify a consumer complaint into one of six domains: "
37
  "ecommerce, telecom, banking, cibil, insurance, or general. "
38
+ "MUST be the very first tool called on every new complaint thread. "
39
+ "The result includes a 'low_confidence' boolean field. "
40
+ "When low_confidence is true (model confidence < 0.50, or keyword "
41
+ "fallback was used), do NOT proceed with the suggested domain β€” "
42
+ "instead ask the user one clarifying question to confirm the domain "
43
+ "before continuing."
44
  ),
45
  "input_schema": {
46
  "type": "object",
src/classifier/model.py CHANGED
@@ -42,7 +42,8 @@ class DomainResult:
42
  """Classification output for a single complaint."""
43
  domain: str
44
  confidence: float
45
- all_probs: dict # {domain_label: probability}
 
46
 
47
 
48
  # ---------------------------------------------------------------------------
 
42
  """Classification output for a single complaint."""
43
  domain: str
44
  confidence: float
45
+ all_probs: dict # {domain_label: probability}
46
+ low_confidence: bool = False # True when confidence < DOMAIN_CONFIDENCE_THRESHOLD
47
 
48
 
49
  # ---------------------------------------------------------------------------
src/classifier/predict.py CHANGED
@@ -22,6 +22,13 @@ logger = logging.getLogger(__name__)
22
  _DEFAULT_MODEL_DIR = "models/domain_classifier"
23
  _classifier: Optional[DomainClassifier] = None
24
 
 
 
 
 
 
 
 
25
  # ---------------------------------------------------------------------------
26
  # Keyword fallback
27
  # ---------------------------------------------------------------------------
@@ -73,8 +80,9 @@ def _keyword_classify(text: str) -> DomainResult:
73
  logger.debug("Keyword fallback scores: %s β†’ %s", scores, best)
74
  return DomainResult(
75
  domain=best,
76
- confidence=0.0, # sentinel: 0.0 signals keyword fallback, not a model score
77
  all_probs=all_probs,
 
78
  )
79
 
80
 
@@ -113,8 +121,12 @@ def classify(text: str, model_dir: str = _DEFAULT_MODEL_DIR) -> DomainResult:
113
  """
114
  Classify *text* and return a DomainResult.
115
 
116
- If no checkpoint exists at *model_dir*, falls back to keyword heuristics
117
- and returns a DomainResult with confidence=0.0 as a fallback signal.
 
 
 
 
118
  """
119
  global _classifier
120
  if _classifier is None:
@@ -127,4 +139,15 @@ def classify(text: str, model_dir: str = _DEFAULT_MODEL_DIR) -> DomainResult:
127
  )
128
  return _keyword_classify(text)
129
  _classifier = DomainClassifier(model_dir)
130
- return _classifier.predict(text)
 
 
 
 
 
 
 
 
 
 
 
 
22
  _DEFAULT_MODEL_DIR = "models/domain_classifier"
23
  _classifier: Optional[DomainClassifier] = None
24
 
25
+ # Minimum model confidence required to trust the classification result.
26
+ # Below this threshold the result is flagged as low_confidence=True and the
27
+ # CMA agent is expected to ask the user a clarifying domain question instead
28
+ # of proceeding automatically. The keyword fallback (confidence=0.0 sentinel)
29
+ # always sets low_confidence=True regardless of this threshold.
30
+ DOMAIN_CONFIDENCE_THRESHOLD: float = 0.50
31
+
32
  # ---------------------------------------------------------------------------
33
  # Keyword fallback
34
  # ---------------------------------------------------------------------------
 
80
  logger.debug("Keyword fallback scores: %s β†’ %s", scores, best)
81
  return DomainResult(
82
  domain=best,
83
+ confidence=0.0, # sentinel: 0.0 signals keyword fallback, not a model score
84
  all_probs=all_probs,
85
+ low_confidence=True, # always uncertain when falling back to keywords
86
  )
87
 
88
 
 
121
  """
122
  Classify *text* and return a DomainResult.
123
 
124
+ Two sources of low_confidence=True:
125
+ 1. No checkpoint exists β†’ keyword fallback is used (confidence=0.0 sentinel).
126
+ 2. Model exists but top-class probability < DOMAIN_CONFIDENCE_THRESHOLD.
127
+
128
+ Callers (and the CMA agent) must check low_confidence and ask the user a
129
+ clarifying domain question rather than proceeding with an uncertain result.
130
  """
131
  global _classifier
132
  if _classifier is None:
 
139
  )
140
  return _keyword_classify(text)
141
  _classifier = DomainClassifier(model_dir)
142
+
143
+ result = _classifier.predict(text)
144
+
145
+ if result.confidence < DOMAIN_CONFIDENCE_THRESHOLD:
146
+ logger.info(
147
+ "DomainClassifier low confidence (%.2f < %.2f) for domain '%s' β€” "
148
+ "flagging for user clarification.",
149
+ result.confidence, DOMAIN_CONFIDENCE_THRESHOLD, result.domain,
150
+ )
151
+ result.low_confidence = True
152
+
153
+ return result