Spaces:
Runtime error
Runtime error
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 +28 -10
- src/agent/prompts.py +24 -6
- src/agent/tools.py +6 -1
- src/classifier/model.py +2 -1
- src/classifier/predict.py +27 -4
docs/architecture.md
CHANGED
|
@@ -93,21 +93,33 @@ IDs) that are not redacted by Presidio.
|
|
| 93 |
|
| 94 |
| Attribute | Value |
|
| 95 |
|-----------------|----------------------------------------------------|
|
| 96 |
-
| Architecture
|
| 97 |
-
| Task
|
| 98 |
-
| Classes (6)
|
| 99 |
-
| Training data
|
| 100 |
-
| Script
|
| 101 |
-
| Input
|
| 102 |
-
| Output
|
| 103 |
-
|
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
store_memory(key="domain", value=<domain string>)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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,
|
| 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 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|