Prompts the instructions sent to the model — editable here, no
deployment needed
What is and is not decided here. These texts tell the model how to
transcribe a call and how to judge it. They do not decide the score:
every number is recomputed in Python from config/rubric.json, question
counts override whatever the model claims, and a transcript that runs past the end of
its own audio is rejected in code. So an edit changes judgement and wording — it
cannot change the arithmetic, and it cannot switch off the safety checks.
A saved change applies to the next run ; calls already scored stay as
they are until they are re-run.
Transcription
3255 characters ·
built-in, never edited
Sent with the audio. It decides how the recording is turned into English text, who is labelled agent or customer, and the JSON shape the rest of the system reads.
Before you edit. This prompt has invented conversations before. It must never be told the call's expected length or the speakers' names, and it must keep telling the model not to transcribe audio it cannot hear. Recordings under 5 seconds are refused in code, and any transcript running past the end of its audio is rejected in code, whatever this prompt says.
These must stay in the text, because the code that reads the
reply looks for them:
"turns" · "speaker" · text_native · text_en · primary_language · "connected"
The opening instructions for scoring a call against the pitch. The rubric itself - steps, weights, objections - is generated from config/rubric.json and is not part of this text.
Before you edit. Arithmetic is not done by the model: scores are recomputed in Python from the rubric weights, and question counts override whatever label comes back. Editing this changes judgement, not the maths.
You are scoring one JSW One inside-sales call against a fixed, business-approved rubric. You are a strict but FAIR auditor. Your output feeds an agent scorecard, so a wrong score has a real cost to a real person.
THE THREE SIGNALS MUST NEVER MERGE. This is the single most important rule:
- MODULE 1 (pitch adherence) judges ONLY the REP's execution of the pitch structure.
- MODULE 2 (customer quality) describes the LEAD we handed the rep. A junk lead is NOT a rep failure.
- MODULE 3 (supply) describes OUR ability to serve. An empty stockyard is NOT a rep failure.
A call can score a perfect 10 on pitch and still lose, because the lead was unfit or we had no stock. Never let a bad outcome drag down the pitch score, and never let a good outcome inflate it.
SCORING PHILOSOPHY - moderate, not literal:
- A step is PRESENT when its INTENT is executed, even if the words differ completely from the script. Permission obtained, hook delivered, discovery attempted, next step committed.
- The script cues below are how to RECOGNISE a step. They are NOT phrases the rep must say. A verbatim keyword matcher systematically under-scores good reps - do not behave like one.
- Order and phrasing are flexible. Count a discovery question as asked when its intent is covered, in any order, however it was worded.
- Language is not quality: a call conducted entirely in Gujarati or Marathi is scored exactly like an English one.
- EVIDENCE OVER LABELS. The logged disposition is known to be unreliable. Where the transcript contradicts the label, trust the transcript.
- EVERY step result must be backed by a short verbatim quote from the transcript. If you cannot quote evidence for a "pass", it is not a pass.
- Where the transcript is partly "[inaudible]", judge only what is legible and say so in the comment. Do not penalise the rep for ASR gaps.
Save scoring preamble
saved to /app/config/prompts/analysis_header.md
Rules the rubric, the bands and the
lexicon — what the numbers are computed from
These are not wording — they are arithmetic. The prompts above tell the
model what to look for; these files decide what a score is . Every one is checked
before it saves: invalid JSON, a weight that is not a number, a negative weight, a
repeated step id or weights summing to zero are all refused with the offending step
named, and nothing is written.
A saved change applies to new runs at once. Calls already scored keep their old
numbers until they are re-scored — that reads the transcripts already on disk and
costs no tokens:
./cli.py rescore
Pitch rubric
14243 characters · 2026-10-31 15:38
The pitch steps, their weights and the score bands, per pitch type. This is what a score is computed FROM - the model reports which steps happened, and Python does the arithmetic from these weights.
Before you edit. Changing a weight changes every future score, and makes new scores incomparable with old ones. Calls already scored keep their old number until they are re-scored (./cli.py rescore), which recomputes from the transcripts already on disk and costs nothing.
{
"meta": {
"name": "JSW One x Ozonetel Inside-Sales Call Analysis Rubric",
"version": "1.0.0",
"source_brd": "JSW One IS Call Quality BRD (Modules 1-3)",
"scoring_philosophy": "moderate",
"notes": "A step is PRESENT if its intent is executed, not if the exact scripted phrase is used. Ordering and phrasing are flexible; core steps must be present.",
"band_thresholds": { "green_min": 8, "amber_min": 5, "red_min": 0 },
"partial_credit_fraction": 0.5
},
"pitch_type_detection": {
"priority_order": [
{ "type": "customer_led", "rule": "opportunity_id is present / non-empty" },
{ "type": "new_registration", "rule": "registration_date within 30 days of call_datetime AND no prior opportunities" },
{ "type": "supply_led", "rule": "campaign_type == supply_led OR campaign flagged as supply-led (business-named campaigns)" },
{ "type": "baseline", "rule": "redial case OR dormant customer (redial_flag true / dormant_flag true) and none of the above" },
{ "type": "undetermined", "rule": "none of the above can be confirmed -> EXCLUDE from pitch scoring" }
],
"excluded_from_scoring": ["undetermined"]
},
"pitch_rubrics": {
"supply_led": {
"max_score": 10,
"steps": [
{ "id": "SL1", "weight": 2, "name": "Opener & permission", "mandatory": true, "output": "pass_fail",
"checks": "Agent greets, identifies as JSW One, uses customer name, asks if it's a good time. If declined, does not push; asks for callback time." },
{ "id": "SL2", "weight": 2, "name": "Hook (supply/price signal)", "mandatory": true, "output": "pass_fail",
"checks": "Leads with ONE benefit (price OR availability, not both). Hook under ~30s. No immediate product dump." },
{ "id": "SL3", "weight": 2, "name": "Discovery (6 Qs)", "mandatory": true, "output": "full_partial_fail",
"threshold_min": 3, "threshold_full": 5, "of_total": 6,
"checks": "Q1 what customer manufactures, Q2 end applications, Q3 monthly MT, Q4 current brands/suppliers, Q5 JSW% of procurement, Q6 payment terms." },
{ "id": "SL4", "weight": 1, "name": "Pitch (2-3 USPs)", "mandatory": true, "output": "pass_fail",
"checks": "USPs selected relevant to what was discovered. Did NOT pitch all USPs indiscriminately." },
{ "id": "SL5", "weight": 2, "name": "Qualify", "mandatory": true, "output": "pass_fail",
"checks": "Asked if current requirement exists. If yes: captured grade, thickness, width, qty, location, date. Confirmed stock on the spot if possible." },
{ "id": "SL6", "weight": 1, "name": "Closure with next step", "mandatory": true, "output": "pass_fail",
"checks": "Explicit committed next step: quote with agreed turnaround OR specific callback date logged." }
]
},
"customer_led": {
"max_score": 10,
"steps": [
{ "id": "CL1", "weight": 2, "name": "Opener acknowledges platform submission", "mandatory": true, "output": "pass_fail",
"checks": "Acknowledges customer raised a requirement on platform. Does NOT restart from scratch. References what customer did." },
{ "id": "CL2", "weight": 3, "name": "Revalidate every field", "mandatory": true, "output": "full_partial_fail",
"threshold_min": 3, "threshold_full": 5, "of_total": 5,
"checks": "Reads back and confirms: product/grade, thickness x width, quantity (MT), delivery location/pincode, timeline. Changed fields updated before pricing." },
{ "id": "CL3", "weight": 2, "name": "Discovery (5 Qs, woven naturally)", "mandatory": true, "output": "full_partial_fail",
"threshold_min": 2, "threshold_full": 4, "of_total": 5,
"checks": "Q1 what customer manufactures/end application, Q2 monthly MT, Q3 current brands, Q4 JSW% of procurement, Q5 payment terms. Conversational, not interrogative." },
{ "id": "CL4", "weight": 2, "name": "Closure with price & timeline", "mandatory": true, "output": "pass_fail",
"checks": "Committed to specific delivered price OR confirmed stock and pushed for immediate closure. Timeline for price revert stated. If stock confirmed, attempted immediate close." },
{ "id": "CL5", "weight": 1, "name": "No unnecessary re-pitching", "mandatory": true, "output": "pass_fail",
"checks": "Did NOT re-pitch entire portfolio. Acknowledged customer already chose platform. Reinforced only 1-2 relevant advantages." }
]
},
"new_registration": {
"max_score": 10,
"steps": [
{ "id": "NR1", "weight": 1, "name": "Warm welcome opener", "mandatory": true, "output": "pass_fail",
"checks": "Warm, welcomes new registration, positions as helper not pusher. No hard sales pressure in opener." },
{ "id": "NR2", "weight": 1, "name": "Verify basic profile (4 Qs)", "mandatory": true, "output": "full_partial_fail",
"threshold_min": 2, "threshold_full": 4, "of_total": 4,
"checks": "Q1 name/company, Q2 procurement city, Q3 involvement in procurement decisions, Q4 best contact number/email for quotes." },
{ "id": "NR3", "weight": 3, "name": "Full discovery (8 Qs)", "mandatory": true, "output": "full_partial_fail",
"threshold_min": 4, "threshold_full": 6, "of_total": 8,
"checks": "Q1-2 business type/end applications, Q3-4 monthly MT/brands, Q5 payment terms, Q6 current JSW buying, Q7 JSW% of procurement, Q8 what stopped direct JSW buying earlier." },
{ "id": "NR4", "weight": 1, "name": "Platform education", "mandatory": true, "output": "pass_fail",
"checks": "Explained platform simply: transparent delivered pricing, Test Certificate, stockyard availability. Tailored to discovery." },
{ "id": "NR5", "weight": 2, "name": "First opportunity identified", "mandatory": true, "output": "pass_fail",
"checks": "Asked if current material requirement. If yes: full spec captured. If no: specific callback date scheduled (not 'sometime')." },
{ "id": "NR6", "weight": 2, "name": "Closure - one of three outcomes", "mandatory": true, "output": "pass_fail",
"checks": "Ended with exactly one of: (1) quote sent + follow-up time agreed, (2) specific callback date confirmed in CRM, (3) platform walkthrough scheduled with time agreed." }
]
},
"baseline": {
"max_score": 10,
"inferred": true,
"inferred_note": "BRD names a Baseline pitch for redial/dormant cases but does not define its rubric. Derived here from the shared core of the three defined pitches. FLAGGED as an assumption for business sign-off.",
"steps": [
{ "id": "BL1", "weight": 2, "name": "Opener & permission", "mandatory": true, "output": "pass_fail",
"checks": "Greets, identifies as JSW One, uses customer name, asks if it's a good time." },
{ "id": "BL2", "weight": 2, "name": "Re-establish context", "mandatory": true, "output": "pass_fail",
"checks": "References prior interaction / dormant relationship; does not cold-restart as if never contacted." },
{ "id": "BL3", "weight": 3, "name": "Discovery / requirement refresh", "mandatory": true, "output": "full_partial_fail",
"threshold_min": 2, "threshold_full": 4, "of_total": 5,
"checks": "Refreshes: what customer manufactures, monthly MT, current brands, JSW% of procurement, payment terms." },
{ "id": "BL4", "weight": 1, "name": "Relevant value reinforcement", "mandatory": true, "output": "pass_fail",
"checks": "Reinforced 1-2 relevant advantages; did not dump full portfolio." },
{ "id": "BL5", "weight": 2, "name": "Closure with next step", "mandatory": true, "output": "pass_fail",
"checks": "Explicit committed next step: quote with turnaround OR specific callback date." }
]
}
},
"score_bands": [
{ "band": "green", "label": "Strong", "min": 8, "max": 10, "action": "No action. Positive flag for scorecard." },
{ "band": "amber", "label": "Partial", "min": 5, "max": 7, "action": "Flag for coaching. Supervisor review within 1 week." },
{ "band": "red", "label": "Low", "min": 0, "max": 4, "action": "Urgent coaching. Supervisor listens within 48 hrs." }
],
"customer_quality_flags": {
"note": "Independent flags. 0-4 can coexist. Classification, not a score.",
"dimensions": [
{ "id": "segment", "flag_field": "mfg_flag", "detail_field": "segment_detected",
"values": ["manufacturing", "construction", "trader", "cement", "wrong_product", "unknown"],
"flag_true_when": "segment_detected != manufacturing",
"checks": "Manufacturer -> MFG (target). Construction co/contractor -> construction. Cement dealer/trader/retailer -> trader/cement. Stainless/alloy/pipes only -> wrong_product. Unclear -> unknown." },
{ "id": "geo", "flag_field": "geo_flag", "detail_field": "geo_reason",
"values": ["language_barrier", "policy_excluded", "unknown_area", null],
"flag_true_when": "agent states area not serviceable OR customer in known out-of-coverage state/city OR language breakdown",
"checks": "Non-serviceable area, out-of-coverage geography (e.g. J&K, Kolkata for certain products), or language switch with communication breakdown." },
{ "id": "moq", "flag_field": "moq_flag", "detail_field": "moq_qty_detected", "severity_field": "moq_severity",
"severity_values": ["standard", "critical", "none"],
"flag_true_when": "quantity <= 5 MT OR agent mentions MOQ constraint OR sub_lost_reason == 'A- Lower than MOQ'",
"severity_rule": "critical if < 3 MT, else standard if <= 5 MT, else none",
"checks": "<=5 MT flagged; <3 MT critical." },
{ "id": "material", "flag_field": "material_flag", "detail_field": "material_detected", "status_field": "catalogue_status",
"status_values": ["available", "not_available", "unknown"],
"flag_true_when": "material outside catalogue or not available",
"checks": "Stainless steel, alloy steel, pipes, cement (where unavailable), or a spec agent confirms cannot be sourced." }
]
},
"supply_signal": {
"field": "supply_flag",
"note": "Module 3 = OUR ability to serve. Tracked separately from Module 2 (customer quality). Single categorical value per call.",
"values": [
{ "value": "out_of_stock", "checks": "Agent states material not available, or customer told to wait for replenishment." },
{ "value": "lead_time_issue", "checks": "Customer needs material within a timeline agent cannot meet from stock/standard mill lead times.", "extra_fields": ["customer_req_date", "agent_committed_date"] },
{ "value": "not_in_stockyard", "checks": "Material in catalogue but not in JSW One stockyard; longer mill lead time." },
{ "value": "spec_not_available", "checks": "Exact spec (thickness, width, grade) not carried; customer spec cannot be met.", "extra_fields": ["spec_details"] },
{ "value": "none", "checks": "No supply constraint mentioned." }
]
},
"output_schema": {
"note": "Per-call analysis object Claude returns (BRD 8.1). Deterministic scorer emits the same shape.",
"fields": [
"call_id", "agent_id", "pitch_type_detected", "pitch_adherence_score",
"pitch_step_results", "pitch_missed_steps", "pitch_score_band", "pitch_coaching_note",
"segment_detected", "mfg_flag", "geo_flag", "geo_reason",
"moq_flag", "moq_qty_detected", "moq_severity",
"material_flag", "material_detected", "catalogue_status",
"supply_flag", "supply_details", "quality_flags_total", "call_summary"
]
},
"supplementary_metrics": {
"note": "Agent-performance metrics beyond the 10-point rubric. Measurements (not part of the score), derived deterministically from turn-level timings + content. Added per business request.",
"conversation_dynamics": ["talk_ratio", "agent_talk_sec", "customer_talk_sec", "dead_air_sec",
"longest_monologue_sec", "agent_wpm", "interruptions"],
"sales_effectiveness": ["opener_compliant", "discovery_qs", "qualify_reached",
"next_step_committed", "objection_raised", "objection_handled"],
"fairness_segmentation": ["adherence_by_persona", "adherence_by_language", "adherence_by_segment"],
"data_quality": ["undetermined_rate", "very_short_call"],
"monologue_heavy_rule": "talk_ratio > 0.72 OR longest_monologue_sec > 55 (upper-tail flag, not a per-call scored step)"
},
"objection_handling": {
"note": "From the revised IS pitches (07.07) objection playbook. Detected as a supplementary metric, NOT a scored rubric step (keeps the 10-point weighting BRD-locked). An objection raised but not handled is a coaching flag.",
"source": "Revised Pitches IS 07.07 — per-pitch OBJECTIONS tables",
"examples": ["price higher -> compare landed cost", "already have supplier -> trial order on running size",
"need credit -> structured credit after two transactions", "remote site -> logistics partners"]
},
"provenance": {
"pitch_scripts": "Revised Pitches IS 07.07 (Supply-Led / Customer-Led / New-Registration). Rubric structure confirmed unchanged; script language adopted for detection markers.",
"personas": "Customer Personas for JSW One's B2B Buyers in India — 5 personas (Fabrication MSME, Light Engineering SMB, Infra Contractor, Real-Estate Builder, Retailer/Stockist) drive segment, product, MT range, language, geography and objections in the synthetic data."
},
"acceptance_criteria": {
"note": "BRD 10 - used by verify_sample.py to compute agreement vs ground truth / live-Claude.",
"targets": [
{ "id": "AC1", "metric": "pitch_type_accuracy", "target": 0.90 },
{ "id": "AC2", "metric": "pitch_score_agreement_within_points", "target": 1.5 },
{ "id": "AC3", "metric": "customer_quality_flag_accuracy", "target": 0.85 },
{ "id": "AC4", "metric": "supply_flag_accuracy", "target": 0.85 },
{ "id": "AC5", "metric": "vernacular_transcription_quality", "target": 0.80 },
{ "id": "AC7", "metric": "call_summary_usefulness", "target": 0.80 }
]
}
}
Save pitch rubric
saved to /app/config/rubric.json; the previous version is
kept under config/history/
Objections
3108 characters · 2026-10-31 15:38
The objection types the scorer recognises and how each is judged.
Before you edit. Affects objection handling rates on the overview and campaign pages.
{
"_source": "pitch-adherence-rules.md S7.1 - objection playbooks (detection cues)",
"_note": "Supplementary metric. Never scored inside the /10 - an objection raised but not handled is a coaching flag.",
"types": [
{ "id": "price", "applies_to": ["supply_led","new_registration","customer_led","baseline"],
"customer_cue": "your price is higher / prices won't be competitive / mehnga / costly",
"adherent_response": "compare LANDED cost (freight + loading included); ask which brand; counter with market data; offer a delivered price on their typical size" },
{ "id": "incumbent_supplier", "applies_to": ["supply_led","new_registration","baseline"],
"customer_cue": "I already have a supplier / happy with my current supplier",
"adherent_response": "not asking to switch - propose ONE trial order on a running size; judge on TC quality + delivery speed" },
{ "id": "credit", "applies_to": ["supply_led","baseline"],
"customer_cue": "I need credit / payment terms",
"adherent_response": "structured credit AFTER two successful transactions; understand volumes first" },
{ "id": "send_price_will_think", "applies_to": ["customer_led","supply_led","baseline"],
"customer_cue": "send me the price, I'll think / soch ke bataata hoon",
"adherent_response": "send quote immediately; offer to HOLD STOCK 24h; pricing linked to current availability" },
{ "id": "specs_may_change", "applies_to": ["customer_led"],
"customer_cue": "my specs might change",
"adherent_response": "no problem - share revised specs, re-price immediately; confirm updated details now" },
{ "id": "why_call_used_app", "applies_to": ["customer_led"],
"customer_cue": "why are you calling, I put it on the app?",
"adherent_response": "platform captured it perfectly; calling only to secure best price + fastest dispatch; two minutes" },
{ "id": "check_with_manager", "applies_to": ["customer_led","new_registration"],
"customer_cue": "need to check with my manager",
"adherent_response": "offer to copy the manager on the quote; agree a SPECIFIC follow-up time" },
{ "id": "just_registered_to_check", "applies_to": ["new_registration"],
"customer_cue": "just registered to check",
"adherent_response": "offer current HR pricing as a reference; no pressure; reach out when ready" }
],
"usp_triggers": [
{ "trigger": "cost / price", "lead_with": "all-inclusive delivered price - compare landed cost, not base price" },
{ "trigger": "speed / lead time", "lead_with": "own stockyard - same-day / next-day dispatch for running sizes" },
{ "trigger": "quality", "lead_with": "Test Certificate on every dispatch, direct from JSW" },
{ "trigger": "product range", "lead_with": "heavy HR & CR across the full thickness and width range" },
{ "trigger": "credit (only if customer raises it)", "lead_with": "structured credit after two transactions" },
{ "trigger": "after-sales", "lead_with": "single point of contact - no dealer middlemen" }
]
}
Save objections
saved to /app/config/objections.json; the previous version is
kept under config/history/
Analysability bands
1143 characters · 2026-10-31 15:38
How a recording is judged scoreable at all - full conversation, short or unclear, or no conversation. Not a judgement of the agent.
Before you edit. Loosening this pushes thin calls into scoring, where they score low for lack of material rather than for anything the rep did.
{
"_source": "call-sentiment-rules.md S3, refined 2026-08-28.",
"_scope": "Bands OUR Gemini transcript. Ozonetel's own ASR is not read, not banded, not compared.",
"_why_refined": "The original rule keyed on SCRIPT: only Hindi and Latin/English counted as 'recoverable', everything else was 'gist-tier' amber. That made sense when the transcript came from Ozonetel's ASR, where a vernacular script genuinely meant unintelligible text. Against a Gemini transcript - clean native text plus an English translation - it is simply wrong: a perfect Gujarati call was being capped at amber and 0.55 confidence for being Gujarati. We band on measured evidence of transcription quality instead, which is what acceptance criterion 5 intends. Language must not predict score; the dashboard keeps a language x band fairness check to prove it.",
"min_turns_content": 4,
"min_turns_green": 6,
"red_if_not_connected": true,
"inaudible_ratio_red": 0.5,
"inaudible_ratio_amber": 0.2,
"amber_audio_quality": ["very poor"],
"inaudible_marker": "[inaudible]",
"dead_dispositions": ["Invalid_Number", "Not_Answered", "Ringing_NoResponse"]
}
Save analysability bands
saved to /app/config/banding.json; the previous version is
kept under config/history/
Campaign to pitch type
2983 characters · 2026-10-31 15:38
Which pitch rubric each campaign is scored against. An unmatched campaign is transcribed but excluded from pitch scoring, rather than scored against a guessed rubric.
Before you edit. Most calls route here off a naming inference that business has not signed off. Mapping a campaign to the wrong pitch type scores those reps against a script they were never asked to follow.
{
"_note": "Pitch-type detection per pitch-adherence-rules.md S3 needs opportunity_id / registration_date, which the Ozonetel export does NOT carry. Derived here from the campaign naming convention instead. ASSUMPTION - needs business sign-off. Each entry records the evidence so the inference is auditable.",
"_precedence": "An explicit entry here wins. Unmatched campaigns fall through to 'undetermined' and are EXCLUDED from pitch scoring rather than defaulted to baseline.",
"campaigns": {
"IS_MFG_HR_HRPO_PM_Opp_Jul26": {
"pitch_type": "customer_led",
"evidence": "'_Opp_' = opportunity already raised on the platform",
"confidence": "medium"
},
"IS_MFG_CR_GI_Coated_Opp_Jul26": {
"pitch_type": "customer_led",
"evidence": "'_Opp_' = opportunity already raised on the platform",
"confidence": "medium"
},
"IS_MFG_GP_Opp_Jul26": {
"pitch_type": "customer_led",
"evidence": "'_Opp_' = opportunity already raised on the platform",
"confidence": "medium"
},
"MSME_IS_India_ProductOpps_PROG_261225": {
"pitch_type": "customer_led",
"evidence": "'ProductOpps' = product opportunities raised",
"confidence": "medium"
},
"MSME_IS_PANIndia_NewReg_260526": {
"pitch_type": "new_registration",
"evidence": "'NewReg' = newly registered customers",
"confidence": "high"
},
"IS_MFG_M1_3Reg_No_Opp_Jul26": {
"pitch_type": "new_registration",
"evidence": "'Reg' + 'No_Opp' = registered, no opportunity yet",
"confidence": "medium"
},
"IS_MFG_Integrated_Click_Jul26": {
"pitch_type": "supply_led",
"evidence": "business-named campaign, no Opp/Reg marker -> proactive outreach",
"confidence": "low"
},
"Platinum_Insurance": {
"pitch_type": "out_of_scope",
"evidence": "insurance, not steel inside-sales",
"confidence": "high"
},
"TEST_Campaign_Ignore": {
"pitch_type": "out_of_scope",
"evidence": "test row",
"confidence": "high"
},
"IS_MFG_WR_Sep26": {
"pitch_type": "supply_led",
"evidence": "mapped from the dashboard",
"confidence": "medium"
},
"IS_MFG_HR_HRPO_Sep26": {
"pitch_type": "supply_led",
"evidence": "mapped from the dashboard",
"confidence": "medium"
},
"IS_Inte_MFG_M1_3_NoEnq_Sep26": {
"pitch_type": "new_registration",
"evidence": "mapped from the dashboard",
"confidence": "medium"
},
"IS_MFG_HR_HRPO_Opp_Aug26": {
"pitch_type": "supply_led",
"evidence": "mapped from the dashboard",
"confidence": "medium"
},
"IS_MFG_CR_GI_GP_Coated_Sep26": {
"pitch_type": "supply_led",
"evidence": "mapped from the dashboard",
"confidence": "medium"
},
"MSME_IS_India_ProductOpps_Reg_Acc": {
"pitch_type": "customer_led",
"evidence": "mapped from the dashboard",
"confidence": "medium"
}
}
}
Save campaign to pitch type
saved to /app/config/campaign_map.json; the previous version is
kept under config/history/
Sentiment lexicon
5325 characters · 2026-10-31 15:38
The words and phrases that move customer sentiment, with weights. Sentiment is computed from this list alone - no model is involved, so the same words always give the same answer.
Before you edit. Short common words are dangerous here: 'no' once matched inside know, now, nothing and afternoon and produced 54% false positives until matching was made word-boundary aware.
{
"_source": "call-sentiment-rules.md v2.0.0 S5. Weights: mild=1, strong=2, sales-critical=3.",
"_matching": "Word-boundary match for Latin-script terms, substring match for Indic-script terms. The original rules specified substring everywhere, because OZONETEL's ASR ran words together. Against a clean Gemini transcript that is wrong: 54% of matches for the term 'no' were false positives (now/know/nothing/another/afternoon). Indic terms keep substring matching, where the original rationale still holds.",
"_formula": {
"lexical": "(pos - neg) / (pos + neg + 2)",
"blend_both": "0.6 * lexical + 0.4 * prior",
"blend_lex_only": "lexical",
"blend_prior_only": "0.7 * prior",
"blend_neither": 0.0,
"clamp": [
-1,
1
],
"labels": {
"positive": "> 0.15",
"neutral": "-0.15 .. 0.15",
"negative": "< -0.15"
},
"confidence_base": {
"green": 0.85,
"amber": 0.55
},
"confidence_per_hit": 0.02,
"confidence_cap": 0.95,
"confidence_report": {
"high": 0.75,
"med": 0.45
},
"confidence_hit_cap": 8,
"_confidence_note": "Was base + 0.05/hit, tuned for short garbled transcripts with 2-3 hits. Clean transcripts average 13 hits (max 96), so 243 of 250 calls read 'high' and confidence stopped discriminating. Lower increment plus a hit cap restores the spread."
},
"positive": {
"3": [],
"2": [
"thank you",
"thanks",
"धन्यवाद",
"આભાર",
"interested",
"इंटरेस्टेड",
"perfect",
"great",
"definitely",
"absolutely",
"yes yes",
"haan haan",
"हाँ हाँ",
"छान",
"નક્કી",
"yes, dispatch",
"confirm the order",
"go ahead",
"order confirm"
],
"1": [
"sure",
"good",
"okay",
"ok",
"correct",
"fine",
"please",
"connect",
"share",
"whatsapp",
"send",
"अच्छा",
"ठीक है",
"सही",
"બરાબર",
"बरं",
"send the quote",
"share the price",
"whatsapp me",
"quotation भेज",
"भेज दीजिए"
]
},
"negative": {
"3": [
"no requirement",
"not interested",
"not possible",
"does not deal",
"no need",
"नहीं चाहिए",
"कोई रिक्वायरमेंट नहीं",
"पॉसिबल नहीं",
"जरूरत नहीं",
"કોઈ જરૂર નથી"
],
"2": [
"busy",
"problem",
"issue",
"expensive",
"costly",
"complaint",
"cannot",
"cement",
"मुश्किल",
"दिक्कत",
"समस्या",
"महंगा",
"परेशान",
"મોંઘું",
"your price is higher",
"price is higher",
"prices won't be competitive",
"already have a supplier",
"have a supplier",
"happy with my current supplier"
],
"1": [
"later",
"small",
"sorry",
"wrong",
"no",
"not",
"don't",
"छोटा",
"फिलहाल",
"माफ",
"send me the price",
"i'll think",
"सोच के बताता",
"सोचकर बता",
"check with my manager",
"manager से पूछ",
"just registered to check",
"why are you calling"
]
},
"neutral_procedural": [
"specs might change",
"spec change"
],
"objection_cues": {
"price": [
"your price is higher",
"price is higher",
"prices won't be competitive",
"expensive",
"costly",
"महंगा",
"મોંઘું"
],
"incumbent_supplier": [
"already have a supplier",
"have a supplier",
"happy with my current supplier"
],
"send_price_will_think": [
"send me the price",
"i'll think",
"सोच के बताता",
"सोचकर बता"
],
"check_with_manager": [
"check with my manager",
"manager से पूछ"
],
"just_registered_to_check": [
"just registered to check",
"why are you calling"
],
"specs_may_change": [
"specs might change",
"spec change"
],
"rejection": [
"no requirement",
"does not deal",
"not interested",
"नहीं चाहिए",
"जरूरत नहीं"
]
},
"disposition_priors": {
"Onboarded": 0.75,
"Interested": 0.65,
"Call_back_later": 0.1,
"General Enquiry": 0.0,
"Wrap up time exceeded :0": 0.0,
"No_requirement": -0.25,
"Language_Barrier": -0.35,
"Does_not_deal_Steel": -0.45,
"Not_Answered": null,
"Ringing_NoResponse": null,
"Invalid_Number": null,
"": null
},
"_prior_note": "null = no-contact disposition, carries no prior. Values beyond those named in call-sentiment-rules.md S2 (Call_back_later, General Enquiry, Wrap up) are inferred and flagged as such.",
"_filler_note": "Conversational glue carries no sentiment in Indian B2B calls - every call is full of 'okay', 'sure', 'ठीक है'. They were 75% of all lexicon hits and drowned the 3% of terms that actually mean something. Scored at reduced weight so they still nudge, rather than dominate.",
"filler_terms": [
"okay",
"ok",
"sure",
"fine",
"correct",
"good",
"please",
"ठीक है",
"अच्छा",
"सही",
"બરાબર",
"बरं",
"no",
"not",
"yes"
],
"filler_weight_multiplier": 0.25,
"closing_turns_fraction": 0.3,
"closing_weight_multiplier": 2.0
}
Save sentiment lexicon
saved to /app/config/sentiment_lexicon.json; the previous version is
kept under config/history/