You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Release v3.5.0: serve the panel's conclusion, not only its concordance rate
The 2026-08-25 upload that added Qwen also added four columns nothing served:
consensus_polarite, consensus_centralite, consensus_subjectivite_score and
sentiment_disagreement. v3.4.0 left them alone because they are a new tool
surface rather than part of serving a fifth model. This adds them.
The reason they belong next to what v3.4.0 built is that they disagree with it
about abstentions. agreement.scored_by_all counts articles EVERY model scored,
so the 200 Qwen never annotated leave it entirely. The consensus threshold
instead follows the votes actually cast, over half with a minimum of two, so
those articles are still decided by the four models that did answer. The two are
counted on different sets and their totals are not meant to reconcile; both now
say so in the payload.
get_sentiment_distribution takes model="consensus", and model="all" carries a
consensus block alongside agreement so a comparison call answers both what the
models said and what the panel concluded. consensus is NOT routed through
resolveSentimentModel and is not a member of SENTIMENT_MODELS: no annotator
produced these values, so admitting them to the registry would put a derived
aggregate into a `model` field that everywhere else names the exact model that
judged. A unit test pins that, along with the fact that "consensus" is not a
retired handle either, having never named an annotator at all.
The three fields do not behave alike, and treating them alike is the trap the
implementation is shaped around. Polarity and centrality are majority labels
whose empty value means NO MAJORITY FORMED, never "not computed": 429 and 465
articles respectively, each flagged in the dispute column. Those two populations
partition the empties exactly, the rest being the 51 articles nobody scored.
Subjectivity is a float median rank rather than a label, so it resolves whenever
anyone voted and therefore covers MORE articles than either majority field,
12,195 against 11,869 and 11,833. An even number of voters puts it between two
labels: 117 articles carry a half-rank of 1.5, 2.5, 3.5 or 4.5, which correspond
to no label at all. It is reported as a numeric summary with its own note and
never joined to SUBJECTIVITY_VALUES.
search_by_sentiment gains a `disputed` filter over the pipe-joined dispute
column, taking the stored French field names so that the English spellings fail
loudly with valid_values rather than matching nothing. It turns the contested
articles from a count into something readable, which matters most where a single
model's label looks settled and is not.
The comparison chart gets the consensus in its own panel row rather than among
the model rings, where it would read as a sixth annotator, and a note stating
its own base and that no model produced it.
The fixture encodes each of these rather than restating them. Row 103 splits
polarity two-two-one so no majority forms while centrality on the same row is
unanimous, which is why the fields are stored apart. Row 104 is the row Qwen
skipped: its polarity is still decided by the four remaining votes, so it sits
in the consensus while being absent from agreement.scored_by_all, and row 103 is
the reverse. Subjectivity resolves on all six fixture rows where the label
fields reach five, so coverage.subjectivity > coverage.polarity is a shape a
test protects. Half-ranks need an even split and cannot occur on a six-row
fixture voting five-at-a-time without making the votes contradict each model's
documented abstention policy, so the live smoke test covers them against the 117
real articles that have one, and also checks that the disputed filter returns
exactly the count the aggregate reported.
Token footprint 15,071 to 15,210 of a 16,000 ceiling, re-baselined.
Copy file name to clipboardExpand all lines: .agents/skills/iwac-mcp/SKILL.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -180,7 +180,7 @@ Thirteen tools carry their own interactive chart on hosts that support MCP Apps
180
180
**Actions:**
181
181
1. Cross-reference MCP findings across subsets (articles vs. publications vs. references vs. documents vs. index)
182
182
2. Use `get_sentiment_distribution` with `subject` filter to compare topic-specific sentiment against the collection baseline (e.g., `subject="Laïcité", country="Burkina Faso"` vs. the whole BF corpus)
183
-
3. Before any sentiment claim carries weight, re-run it with `model="all"`. All five models agree unanimously on polarity for only **32%** of the corpus (3,929 of the 12,098 articles they all scored); where your slice diverges further, say so rather than quoting one model
183
+
3. Before any sentiment claim carries weight, re-run it with `model="all"`. All five models agree unanimously on polarity for only **32%** of the corpus (3,929 of the 12,098 articles they all scored); where your slice diverges further, say so rather than quoting one model. That call now also returns the panel's `consensus`, which is what to quote when the claim is about the corpus rather than about a model — and `search_by_sentiment(disputed="polarite")` reads the 429 articles where no majority formed at all
184
184
4. Use `search_articles` results (which include sentiment inline) to build topic-specific sentiment tables without extra calls
185
185
5. Use `get_temporal_distribution` (optionally `group_by=country|newspaper`) to verify a claimed trend over time and compare trajectories across countries or outlets without paging. For an observance claim, `granularity="lunar_month"` is the test: it pools every year into the twelve lunar months, so a Ramadan or Dhu al-Hijja effect either stands above the even split or it does not
186
186
6. Test whether a theme you have named is really distinct: `get_cooccurrence(field="subject")` shows what it is always discussed alongside, and `get_similar_items` on a key article shows whether your "finding" is one story reprinted across several outlets (scores ≥0.85)
@@ -215,7 +215,7 @@ Thirteen tools carry their own interactive chart on hosts that support MCP Apps
215
215
216
216
**For null results:**`Search for [term] in [tool] with [parameters] returned 0 results.`
217
217
218
-
**For AI sentiment findings:** Five models scored the corpus independently — `gpt-5-6-luna` (the default), `mistral-small-2603`, `deepseek-v4-flash-0731`, `gemma-4-31b-it` and `qwen3-8-27b`; the result keys are `polarity`, `centrality`, `subjectivity`. They do **not** all cover the same articles — `qwen3-8-27b` scores 12,098 where the rest score 12,298, a deliberate gap concentrated on articles peripheral to Islam — so read each model's `coverage` before comparing counts, and never present Qwen's shortfall as a defect. **Name the exact model in any reported figure** — the tools echo the model id precisely so a number can be attributed, and the generation-1 models these replaced are refused rather than substituted (`gemini` included: Google's generation-2 member is Gemma, a different model line, so the handle errors instead of resolving). Treat `subjectivity` as the ordinal **label** it is (Très objectif → Très subjectif), never as a percentage, and prefer not to report it at all: it is much less reliable than the other two scales. When comparing topics or countries, use `get_sentiment_distribution` with a `subject` filter for aggregate data, or tabulate the sentiment columns from `search_articles` results. See `references/biases-and-limitations.md` §4 before any sentiment claim carries an argument.
218
+
**For AI sentiment findings:** Five models scored the corpus independently — `gpt-5-6-luna` (the default), `mistral-small-2603`, `deepseek-v4-flash-0731`, `gemma-4-31b-it` and `qwen3-8-27b`; the result keys are `polarity`, `centrality`, `subjectivity`. They do **not** all cover the same articles — `qwen3-8-27b` scores 12,098 where the rest score 12,298, a deliberate gap concentrated on articles peripheral to Islam — so read each model's `coverage` before comparing counts, and never present Qwen's shortfall as a defect. `model="consensus"` gives the panel's precomputed majority, which is **not a sixth model** and must be attributed to the panel rather than to any annotator; its empty values mean *no majority formed*, and its subjectivité is a float median rank rather than a label. **Name the exact model in any reported figure** — the tools echo the model id precisely so a number can be attributed, and the generation-1 models these replaced are refused rather than substituted (`gemini` included: Google's generation-2 member is Gemma, a different model line, so the handle errors instead of resolving). Treat `subjectivity` as the ordinal **label** it is (Très objectif → Très subjectif), never as a percentage, and prefer not to report it at all: it is much less reliable than the other two scales. When comparing topics or countries, use `get_sentiment_distribution` with a `subject` filter for aggregate data, or tabulate the sentiment columns from `search_articles` results. See `references/biases-and-limitations.md` §4 before any sentiment claim carries an argument.
219
219
220
220
**For the evidence-base ledger (open every synthesis with one):** Report three tiers separately — items **read in full**, items **triaged on an AI abstract/snippet only**, and matches **surveyed by count only**. Example: `Evidence base: 4 articles + 1 document read in full; 18 articles triaged on description_ai; 1,909 keyword matches surveyed by count.` This keeps full-text evidence visibly distinct from AI-derived (abstract/snippet) evidence, which carries weaker evidential status.
Copy file name to clipboardExpand all lines: .agents/skills/iwac-mcp/references/biases-and-limitations.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,6 +45,14 @@ Five models scored the corpus independently: **`gpt-5-6-luna`**, **`mistral-smal
45
45
46
46
**Always name the model behind a reported figure.** The columns name the exact model that produced them, and so do the tools; vendor shorthand (`chatgpt`, `mistral`, `deepseek`, `gemma`/`google`, `qwen`/`alibaba`) is accepted as input and resolves to the model id, which is what the payload echoes back.
47
47
48
+
**The panel's majority is available as `model="consensus"`, and it is not a sixth model.** No annotator produced it, so a consensus figure is attributed to *the panel*, never to a model. Prefer it to any single model's number when the claim is about the corpus rather than about a model, and prefer it to `agreement` when you want a conclusion rather than a concordance rate. Three things to carry with it:
49
+
50
+
- The majority follows the votes **actually cast** (over half, minimum two), so it decides articles that `agreement` discards — including the 200 Qwen never annotated. The two are counted on different sets; do not reconcile their totals.
51
+
- An empty polarité or centralité consensus means **no majority formed** (429 and 465 articles), never "not computed". Those are the genuinely contested articles, and `search_by_sentiment(disputed="polarite")` reads them. A single model's label on such an article looks settled and is not, which makes them worth quoting *as* disagreement rather than averaging away.
52
+
- The subjectivité consensus is a **float median rank, not a label**, so it resolves for more articles (12,195) than either majority field, and an even number of voters yields a half-rank (1.5, 2.5 …) matching no label at all. Never map it onto the five labels, and never report it as a percentage. It inherits every reliability caveat above: a median of weak judgements is still weak.
53
+
54
+
The panel splits on subjectivité far more than on anything else — 3,184 articles against 429 for polarité and 465 for centralité — which is the same finding as the κ table below, counted a different way.
55
+
48
56
**Generation 1 is gone from these tools.** An earlier campaign (`gemini-3-flash-preview`, `gpt-5-mini`, `ministral-14b-2512`) scored the same corpus and its columns survive on Hugging Face, but the server no longer serves them: asking for one returns an error rather than substituting the same vendor's generation-2 model. The two generations differ in model, in prompt AND in how subjectivity is stored, so a figure from one is not comparable to a figure from the other — if a source quotes a generation-1 number, say which generation it came from rather than presenting it alongside these. **`gemini` is refused too**, and that is not an oversight: Google's generation-2 member is Gemma 4 31B, a different model line, so reading `gemini` as `gemma-4-31b-it` would attribute one model's judgements to another.
Copy file name to clipboardExpand all lines: .agents/skills/iwac-mcp/references/tools-by-phase.md
+15-1Lines changed: 15 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -86,6 +86,8 @@ Search the 4,697 authority records by name.
86
86
87
87
### search_by_sentiment
88
88
Filter articles by **`gpt-5-6-luna`** sentiment labels (exact match, accents optional). One model's reading, not a consensus — say so when reporting, and use `get_sentiment_distribution(model="all")` to see how far the other four agree.
89
+
90
+
-`disputed` (optional, validated) *(v3.5.0+)*: `polarite` | `centralite` | `subjectivite` — keep only the articles the panel **split on** for that field. French field names, as stored; the English spellings error with `valid_values`. This is how you read the contested cases rather than merely counting them: `disputed="polarite"` returns the 429 articles where no majority formed, which are exactly the ones a single model's label would misrepresent as settled. Combines with the country, subject and date filters.
89
91
-`polarity` (optional): Très positif | Positif | Neutre | Négatif | Très négatif | Non applicable
90
92
-`centrality` (optional): Très central | Central | Secondaire | Marginal | Non abordé
91
93
-`subjectivity` (optional): Très objectif | Plutôt objectif | Mixte | Plutôt subjectif | Très subjectif. Filterable since v2.0.0, when generation 2 turned this field from a 1-5 rating into a closed label vocabulary — a numeric value now errors. Unscored where the model answered `Non abordé`, so this filter excludes those rows too. It is the weakest of the three scales: use it to *find* articles to read, not to count them
@@ -228,11 +230,23 @@ Counts of matching items per year (or month) — one call replaces paging throug
228
230
### get_sentiment_distribution *(`model` added v0.13.0; model-exact ids since the 2026-07-31 dataset rename)*
-`model` (optional, validated): `gpt-5-6-luna` (default) | `mistral-small-2603` | `deepseek-v4-flash-0731` | `gemma-4-31b-it` | `qwen3-8-27b` | **all**. Vendor shorthand (`chatgpt` / `mistral` / `deepseek` / `gemma` or `google` / `qwen` or `alibaba`) resolves to these ids, which is what the payload echoes back — quote the id, never the vendor. The generation-1 ids (`gemini-3-flash-preview`, `gpt-5-mini`, `ministral-14b-2512`) are **refused by name**, not substituted, and so is bare `gemini`: Google's generation-2 member is Gemma, a different model line (`qwen` resolves, by contrast, because the Qwen line did score generation 2)
233
+
-`model` (optional, validated): `gpt-5-6-luna` (default) | `mistral-small-2603` | `deepseek-v4-flash-0731` | `gemma-4-31b-it` | `qwen3-8-27b` | **all** | **consensus**. Vendor shorthand (`chatgpt` / `mistral` / `deepseek` / `gemma` or `google` / `qwen` or `alibaba`) resolves to these ids, which is what the payload echoes back — quote the id, never the vendor. The generation-1 ids (`gemini-3-flash-preview`, `gpt-5-mini`, `ministral-14b-2512`) are **refused by name**, not substituted, and so is bare `gemini`: Google's generation-2 member is Gemma, a different model line (`qwen` resolves, by contrast, because the Qwen line did score generation 2)
232
234
- Returns `polarity_distribution`, `centrality_distribution` and `subjectivity` — a distribution over the five French labels plus `scored` / `unscored` and a `mean_rank` / `median_rank` derived by ranking the labels 1-5. That rank is a position on a five-point scale, never a percentage. The block also carries a `caveat`: quote it whenever you quote the number
233
235
- With `model="all"`: `by_model` (each model's distributions), `agreement` (how often they concur on polarity — ten pairwise counts and one unanimous count for five models) and `agreement_matrix` (where the first two part company)
234
236
- Every model block carries a `coverage` object, because the five do **not** score the same articles: `qwen3-8-27b` reaches 12,098 where the others reach 12,298. `agreement` is measured on the articles *all* of them scored, so its `scored_by_all` base — and every pairwise count in it, including pairs Qwen is not part of — is the smaller one, and `base_caveats` names why. Compare proportions, not raw counts, across models
235
237
238
+
### `model="consensus"` — what the panel concluded *(v3.5.0+)*
239
+
240
+
A precomputed majority, served as stored rather than derived at query time, and **not a sixth model**: no annotator produced it, so never attribute a consensus figure to one. It also rides along inside `model="all"` as a `consensus` block, so a comparison call answers both "how far do they agree" and "what did they conclude".
241
+
242
+
The majority threshold follows the votes **actually cast** (over half, minimum two). That is the reason to prefer it over `agreement`: `agreement` counts only articles *every* model scored, so the 200 Qwen skipped drop out of it entirely, while the consensus still decides them on the four remaining votes. The two are counted on different sets and their totals should not be reconciled.
243
+
244
+
The three fields do not behave alike, and this is the part to get right:
245
+
246
+
-`polarity_distribution` / `centrality_distribution` are **majority labels**. An empty value means **no majority formed** (429 and 465 articles), never "not computed". Those articles are absent from the distribution, so read `coverage` for the denominator.
247
+
-`subjectivity_median_rank` is a **float median on the 1-5 scale, not a label**. A median resolves whenever anyone voted, so it covers *more* articles (12,195) than either majority field. An even number of voters yields a **half-rank** (1.5, 2.5 …) matching no label at all — 117 articles have one. Never map it back onto the five labels, and never quote it as a percentage.
248
+
-`disputed` counts the fields the panel split on: polarité 429, centralité 465, subjectivité 3,184, any 3,778 (30.6% of the corpus). On the two label fields a dispute is *why* the consensus is empty; on subjectivité the median still resolved, so a disputed article still carries a value.
249
+
236
250
**Tip:**`get_sentiment_distribution(subject="Laïcité", country="Burkina Faso")` gives the polarity distribution for laïcité articles in BF specifically; compare against the unfiltered country baseline.
237
251
238
252
**Use `model="all"` before quoting any sentiment figure that carries an argument.** Corpus-wide the five models agree unanimously on polarity for only 3,929 of the 12,098 articles they all scored (**32%**). That number is the confidence floor: in a slice where they diverge further, a single model's polarity is a weak claim, and the disagreement is itself reportable. Four models reached 36% and the first three alone 43%, so a figure copied from an older draft will overstate the agreement.
Copy file name to clipboardExpand all lines: mcpb/manifest.json
+3-3Lines changed: 3 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
"manifest_version": "0.3",
3
3
"name": "iwac-mcp-server",
4
4
"display_name": "Islam West Africa Collection (IWAC)",
5
-
"version": "3.4.0",
5
+
"version": "3.5.0",
6
6
"description": "Read-only access to the Islam West Africa Collection via Hugging Face datasets.",
7
7
"long_description": "Search and analyse the Islam West Africa Collection (IWAC): ~12,000 newspaper articles, 1,500 Islamic publications (searchable by keyword, subject, series, and table of contents), 4,700 index entries, plus audiovisual recordings, fieldwork photographs, archival documents, and academic references. Includes AI sentiment analysis (polarity/centrality/subjectivity, scored independently by gpt-5-6-luna, mistral-small-2603, deepseek-v4-flash-0731, gemma-4-31b-it and qwen3-8-27b), per-article AI abstracts, and per-year coverage timelines. All matching is accent- and case-insensitive.\n\n**No API key is required for the core tools** (cross-subset search/fetch, keyword search, filtering, statistics, timelines, item details). Three optional semantic-search tools use Gemini embeddings — over all articles, the publication series with tables of contents, and the photographs (cross-modal: describe what an image shows) — and need a free Google/Gemini API key; these are disabled by default.\n\nOn first use the server downloads ~250 MB of parquet data from Hugging Face into a local cache.",
8
8
"author": {
@@ -71,11 +71,11 @@
71
71
},
72
72
{
73
73
"name": "search_by_sentiment",
74
-
"description": "Filter articles by gpt-5-6-luna polarity, centrality and subjectivity labels."
74
+
"description": "Filter articles by gpt-5-6-luna polarity, centrality and subjectivity labels, or by the fields the model panel split on."
75
75
},
76
76
{
77
77
"name": "get_sentiment_distribution",
78
-
"description": "Aggregate AI sentiment stats across a filter set, for one of the five models that scored the corpus or for all five plus their agreement. Renders as interactive donut charts in hosts that support MCP Apps."
78
+
"description": "Aggregate AI sentiment stats across a filter set, for one of the five models that scored the corpus for all five plus their agreement, or for the panel's precomputed majority. Renders as interactive donut charts in hosts that support MCP Apps."
0 commit comments