jtlevine Claude Opus 4.7 (1M context) commited on
Commit
86acbca
·
1 Parent(s): 8de17bd

Phase 1 foundation: Kenya migration via REGION toggle

Browse files

Adds Kenya as a sibling region to the existing Tamil Nadu / India
pipeline behind a MARKET_INTEL_REGION env toggle (default: kenya).

- config.py loads markets_kenya.json / commodities_kenya.json /
farmers_kenya.json when REGION=kenya, preserving all India behavior
under REGION=india
- 10 KAMIS wholesale markets (Kibuye, Wakulima Nairobi, Kongowea,
Eldoret, Nakuru Wakulima, Kitengela, Kangemi, Karatina, Kathonzweni,
Kaanwa) with coordinates and commodity routing
- 4 commodities: Dry Maize (KAMIS product_id 1), Beans (30), Red Irish
Potato (57), Green Grams (10). Maize seasonal indices derived from
real 2021-2025 KAMIS cache; others from domain research
- 100 Kenyan farmer personas (mixed Swahili/Kikuyu/Luo/Luhya/Kalenjin
names), deterministic seed=42
- src/ingestion/kamis.py: live scrape + demo snapshot path, ports the
benchmark reference parser (expired-cert handling, 3s polite delay,
retry backoff). Plugs into pipeline._step_ingest as a sibling to
fetch_mandi_prices behind REGION dispatch
- src/rag/knowledge_base_kenya.py: 24 chunks across 6 categories
(crop calendars, market institutions, post-harvest, transport,
storage, shocks) with 20 distinct sources (FEWS NET, WFP, KALRO,
CIMMYT, Purdue PICS, KACE, etc.). provider.py switches KB by REGION
- India-specific tests (DPI Aadhaar/KCC module, Tamil-name assertions,
15-mandi/8-35-quintal range assertions) gated behind
@skipif(REGION != india) so Kenya pytest passes clean
- 5 new KAMIS ingestion tests: demo-mode smoke, commodity coverage,
ISO-date URL builder, PRODUCT_IDS pin for the 4 Kenya crops, HTML
fixture parser

E2E acceptance:
- MARKET_INTEL_REGION=india pytest tests/ -> 33 passed
- MARKET_INTEL_REGION=kenya pytest tests/ -> 15 passed, 18 India-only skipped
- MARKET_INTEL_REGION=kenya MARKET_INTEL_DEMO_MODE=1 pipeline.run() ->
7 steps ok, 308 KAMIS records, 100 Kenyan farmer recommendations in 7.2s

Phase 1.4 (Swahili translation + agent region-awareness), 1.5 (frontend
localization), 1.6 (region-parameterized fixtures), and 1.7 (Docker
default + CI matrix) still to come.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commodities_kenya.json ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "MZE-DRY",
4
+ "name": "Dry Maize",
5
+ "agmarknet_name": "Dry maize",
6
+ "kamis_product_id": 1,
7
+ "unit": "100kg",
8
+ "category": "cereal",
9
+ "perishability": "low",
10
+ "msp_2025_rs": null,
11
+ "base_price_rs": 8500,
12
+ "storage_rate_per_day": 3.0,
13
+ "spoilage_pct_per_day": 0.0012,
14
+ "harvest_windows": [
15
+ {"season": "long_rains", "months": [8, 9, 10]},
16
+ {"season": "short_rains", "months": [2, 3]}
17
+ ],
18
+ "seasonal_indices": {
19
+ "1": 1.10, "2": 1.13, "3": 1.03, "4": 1.02, "5": 0.88, "6": 0.94,
20
+ "7": 0.99, "8": 0.94, "9": 0.88, "10": 0.92, "11": 1.16, "12": 1.11
21
+ },
22
+ "post_harvest_loss": {"harvesting": 3.0, "threshing": 2.5, "transport": 1.5, "storage_per_month": 2.5},
23
+ "aliases": ["Dry maize", "Maize (Dry)", "White maize", "Mahindi"]
24
+ },
25
+ {
26
+ "id": "BEN-YGN",
27
+ "name": "Beans (Yellow-Green)",
28
+ "agmarknet_name": "Beans",
29
+ "kamis_product_id": 30,
30
+ "unit": "100kg",
31
+ "category": "pulse",
32
+ "perishability": "low",
33
+ "msp_2025_rs": null,
34
+ "base_price_rs": 13500,
35
+ "storage_rate_per_day": 3.5,
36
+ "spoilage_pct_per_day": 0.0010,
37
+ "harvest_windows": [
38
+ {"season": "long_rains", "months": [7, 8]},
39
+ {"season": "short_rains", "months": [1, 2]}
40
+ ],
41
+ "seasonal_indices": {
42
+ "1": 0.90, "2": 0.88, "3": 0.95, "4": 1.02, "5": 1.08, "6": 1.12,
43
+ "7": 0.92, "8": 0.88, "9": 0.95, "10": 1.05, "11": 1.10, "12": 1.15
44
+ },
45
+ "post_harvest_loss": {"harvesting": 2.5, "threshing": 2.0, "transport": 1.0, "storage_per_month": 2.0},
46
+ "aliases": ["Beans (yellow)", "Beans (green)", "Yellow beans", "Maharagwe"]
47
+ },
48
+ {
49
+ "id": "POT-RIR",
50
+ "name": "Red Irish Potato",
51
+ "agmarknet_name": "Potato",
52
+ "kamis_product_id": 57,
53
+ "unit": "100kg",
54
+ "category": "vegetable",
55
+ "perishability": "medium",
56
+ "msp_2025_rs": null,
57
+ "base_price_rs": 4500,
58
+ "storage_rate_per_day": 4.0,
59
+ "spoilage_pct_per_day": 0.0060,
60
+ "harvest_windows": [
61
+ {"season": "long_rains", "months": [7, 8, 9]},
62
+ {"season": "short_rains", "months": [1, 2]}
63
+ ],
64
+ "seasonal_indices": {
65
+ "1": 0.88, "2": 0.85, "3": 0.95, "4": 1.05, "5": 1.15, "6": 1.18,
66
+ "7": 0.95, "8": 0.85, "9": 0.90, "10": 1.00, "11": 1.10, "12": 1.14
67
+ },
68
+ "post_harvest_loss": {"harvesting": 3.5, "threshing": 0.0, "transport": 2.5, "storage_per_month": 6.0},
69
+ "aliases": ["Irish potato", "Potato", "Viazi"]
70
+ },
71
+ {
72
+ "id": "GRG-KEN",
73
+ "name": "Green Grams",
74
+ "agmarknet_name": "Green grams",
75
+ "kamis_product_id": 10,
76
+ "unit": "100kg",
77
+ "category": "pulse",
78
+ "perishability": "low",
79
+ "msp_2025_rs": null,
80
+ "base_price_rs": 16000,
81
+ "storage_rate_per_day": 3.5,
82
+ "spoilage_pct_per_day": 0.0008,
83
+ "harvest_windows": [
84
+ {"season": "long_rains", "months": [7, 8]},
85
+ {"season": "short_rains", "months": [12, 1]}
86
+ ],
87
+ "seasonal_indices": {
88
+ "1": 0.88, "2": 0.92, "3": 1.02, "4": 1.08, "5": 1.12, "6": 1.15,
89
+ "7": 0.92, "8": 0.88, "9": 0.95, "10": 1.05, "11": 1.10, "12": 0.90
90
+ },
91
+ "post_harvest_loss": {"harvesting": 2.5, "threshing": 1.5, "transport": 1.0, "storage_per_month": 1.5},
92
+ "aliases": ["Green gram", "Ndengu", "Mung bean"]
93
+ }
94
+ ]
config.py CHANGED
@@ -27,10 +27,18 @@ FEATURED_FARMERS, ...).
27
  from __future__ import annotations
28
 
29
  import json
 
30
  from dataclasses import dataclass
31
  from pathlib import Path
32
 
33
 
 
 
 
 
 
 
 
34
  # ── Project root / JSON loader ──────────────────────────────────────────
35
 
36
  _ROOT = Path(__file__).resolve().parent
@@ -41,11 +49,24 @@ def _load_json(name: str) -> list[dict]:
41
  return json.load(fh)
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  # ── Commodities ──────────────────────────────────────────────────────────
45
  # The raw dict shape is preserved so downstream code that indexes with
46
  # c["id"], c["name"], c["agmarknet_name"], c["category"], etc. keeps working.
47
 
48
- COMMODITIES: list[dict] = _load_json("commodities.json")
49
  COMMODITY_MAP: dict[str, dict] = {c["id"]: c for c in COMMODITIES}
50
  CATEGORIES = sorted({c["category"] for c in COMMODITIES})
51
 
@@ -97,7 +118,7 @@ class Mandi:
97
  reporting_quality: str # "good", "moderate", "poor"
98
 
99
 
100
- _MARKETS_RAW = _load_json("markets.json")
101
 
102
  MANDIS: list[Mandi] = [
103
  Mandi(
@@ -118,6 +139,10 @@ MANDIS: list[Mandi] = [
118
 
119
  MANDI_MAP: dict[str, Mandi] = {m.mandi_id: m for m in MANDIS}
120
 
 
 
 
 
121
 
122
  # ── API Endpoints ────────────────────────────────────────────────────────
123
 
@@ -154,12 +179,16 @@ class FarmerPersona:
154
  featured: bool = False # Precomputed live in the weekly run for the demo UI
155
 
156
 
157
- # Curated, hand-written personas loaded from farmers.json. These drive the
158
- # screenshots, the tour narrative, and the featured demo cards. Every
159
- # curated persona is marked featured=True so they get precomputed
160
- # RECOMMEND calls every weekly pipeline run.
161
- _FARMERS_RAW = _load_json("farmers.json")
 
162
 
 
 
 
163
  _CURATED_FARMERS: list[FarmerPersona] = [
164
  FarmerPersona(
165
  farmer_id=f["farmer_id"],
@@ -171,9 +200,9 @@ _CURATED_FARMERS: list[FarmerPersona] = [
171
  quantity_quintals=float(f["quantity_quintals"]),
172
  has_storage=bool(f["has_storage"]),
173
  notes=f.get("notes", ""),
174
- featured=True,
175
  )
176
- for f in _FARMERS_RAW
177
  ]
178
 
179
 
@@ -259,3 +288,7 @@ SAMPLE_FARMERS: list[FarmerPersona] = (
259
  # Featured subset: only these farmers get precomputed RECOMMEND calls in the
260
  # weekly pipeline. The rest are available for on-demand computation.
261
  FEATURED_FARMERS: list[FarmerPersona] = [f for f in SAMPLE_FARMERS if f.featured]
 
 
 
 
 
27
  from __future__ import annotations
28
 
29
  import json
30
+ import os
31
  from dataclasses import dataclass
32
  from pathlib import Path
33
 
34
 
35
+ # ── Region toggle ───────────────────────────────────────────────────────
36
+ # Default Kenya per product decision (LastMileBench MI primary panel).
37
+ # Set MARKET_INTEL_REGION=india to load the original Tamil Nadu data.
38
+
39
+ REGION = os.getenv("MARKET_INTEL_REGION", "kenya")
40
+
41
+
42
  # ── Project root / JSON loader ──────────────────────────────────────────
43
 
44
  _ROOT = Path(__file__).resolve().parent
 
49
  return json.load(fh)
50
 
51
 
52
+ # Per-region filenames. Kenya uses *_kenya.json; India (and anything else)
53
+ # uses the original bare filenames so the existing Tamil Nadu pipeline is
54
+ # byte-for-byte unchanged when REGION != "kenya".
55
+ if REGION == "kenya":
56
+ _MARKETS_FILE = "markets_kenya.json"
57
+ _COMMODITIES_FILE = "commodities_kenya.json"
58
+ _FARMERS_FILE = "farmers_kenya.json"
59
+ else:
60
+ _MARKETS_FILE = "markets.json"
61
+ _COMMODITIES_FILE = "commodities.json"
62
+ _FARMERS_FILE = "farmers.json"
63
+
64
+
65
  # ── Commodities ──────────────────────────────────────────────────────────
66
  # The raw dict shape is preserved so downstream code that indexes with
67
  # c["id"], c["name"], c["agmarknet_name"], c["category"], etc. keeps working.
68
 
69
+ COMMODITIES: list[dict] = _load_json(_COMMODITIES_FILE)
70
  COMMODITY_MAP: dict[str, dict] = {c["id"]: c for c in COMMODITIES}
71
  CATEGORIES = sorted({c["category"] for c in COMMODITIES})
72
 
 
118
  reporting_quality: str # "good", "moderate", "poor"
119
 
120
 
121
+ _MARKETS_RAW = _load_json(_MARKETS_FILE)
122
 
123
  MANDIS: list[Mandi] = [
124
  Mandi(
 
139
 
140
  MANDI_MAP: dict[str, Mandi] = {m.mandi_id: m for m in MANDIS}
141
 
142
+ # Region-neutral alias: downstream code that isn't India-specific should
143
+ # prefer MARKETS over MANDIS (the Indian term).
144
+ MARKETS: list[Mandi] = MANDIS
145
+
146
 
147
  # ── API Endpoints ────────────────────────────────────────────────────────
148
 
 
179
  featured: bool = False # Precomputed live in the weekly run for the demo UI
180
 
181
 
182
+ # Curated, hand-written personas loaded from the region's farmers JSON.
183
+ # India: 3 curated personas drive the demo UI; the other 97 are procedurally
184
+ # generated to reach TARGET_FARMER_COUNT. Kenya: the JSON already contains
185
+ # all 100 personas, so every entry is loaded and procedural generation
186
+ # contributes zero additional farmers.
187
+ _FARMERS_RAW = _load_json(_FARMERS_FILE)
188
 
189
+ # Only the first 3 personas are marked featured=True (precomputed
190
+ # RECOMMEND in the weekly pipeline). For India this covers the full
191
+ # curated set; for Kenya it caps precomputed runs at 3 demo cards.
192
  _CURATED_FARMERS: list[FarmerPersona] = [
193
  FarmerPersona(
194
  farmer_id=f["farmer_id"],
 
200
  quantity_quintals=float(f["quantity_quintals"]),
201
  has_storage=bool(f["has_storage"]),
202
  notes=f.get("notes", ""),
203
+ featured=(i < 3),
204
  )
205
+ for i, f in enumerate(_FARMERS_RAW)
206
  ]
207
 
208
 
 
288
  # Featured subset: only these farmers get precomputed RECOMMEND calls in the
289
  # weekly pipeline. The rest are available for on-demand computation.
290
  FEATURED_FARMERS: list[FarmerPersona] = [f for f in SAMPLE_FARMERS if f.featured]
291
+
292
+ # Region-neutral alias: downstream code that isn't India-specific should
293
+ # prefer FARMERS over SAMPLE_FARMERS.
294
+ FARMERS: list[FarmerPersona] = SAMPLE_FARMERS
data/demo_snapshots/kamis/kamis_demo_snapshot.csv ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ State,District,Market,Commodity,Variety,Min Price,Max Price,Modal Price,Arrivals (Tonnes),Price Date
2
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,17/04/2026
3
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,17/04/2026
4
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,17/04/2026
5
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,17/04/2026
6
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,16/04/2026
7
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,16/04/2026
8
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,16/04/2026
9
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,16/04/2026
10
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,15/04/2026
11
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,15/04/2026
12
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,15/04/2026
13
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,15/04/2026
14
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,14/04/2026
15
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,14/04/2026
16
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,14/04/2026
17
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,14/04/2026
18
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,13/04/2026
19
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,13/04/2026
20
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,13/04/2026
21
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,13/04/2026
22
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,10/04/2026
23
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,10/04/2026
24
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,10/04/2026
25
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,10/04/2026
26
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,09/04/2026
27
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,09/04/2026
28
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,09/04/2026
29
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,09/04/2026
30
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,07/04/2026
31
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,07/04/2026
32
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,07/04/2026
33
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,07/04/2026
34
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,02/04/2026
35
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,02/04/2026
36
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,02/04/2026
37
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,02/04/2026
38
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,01/04/2026
39
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,01/04/2026
40
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,01/04/2026
41
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,01/04/2026
42
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,31/03/2026
43
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,31/03/2026
44
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,31/03/2026
45
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,31/03/2026
46
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,30/03/2026
47
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,30/03/2026
48
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,30/03/2026
49
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,30/03/2026
50
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,27/03/2026
51
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,27/03/2026
52
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,27/03/2026
53
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,27/03/2026
54
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,26/03/2026
55
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,26/03/2026
56
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,26/03/2026
57
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,26/03/2026
58
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,26/03/2026
59
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,26/03/2026
60
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,26/03/2026
61
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,26/03/2026
62
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,25/03/2026
63
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,25/03/2026
64
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,25/03/2026
65
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,25/03/2026
66
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,24/03/2026
67
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,24/03/2026
68
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,24/03/2026
69
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,24/03/2026
70
+ Nairobi,Nairobi,Kangemi Market,Dry maize,,5000.0,5000.0,5000.0,,23/03/2026
71
+ Nairobi,Nairobi,Kangemi Market,Beans (yellow-green),,15000.0,15000.0,15000.0,,23/03/2026
72
+ Nairobi,Nairobi,Kangemi Market,Red irish potato,,4000.0,4000.0,4000.0,,23/03/2026
73
+ Nairobi,Nairobi,Kangemi Market,Green grams,,17500.0,17500.0,17500.0,,23/03/2026
74
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,23/03/2026
75
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,23/03/2026
76
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,23/03/2026
77
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,23/03/2026
78
+ Nairobi,Nairobi,Kawangware,Dry maize,,5500.0,6500.0,6500.0,,19/03/2026
79
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16500.0,19500.0,19500.0,,19/03/2026
80
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4400.0,5200.0,5200.0,,19/03/2026
81
+ Nairobi,Nairobi,Kawangware,Green grams,,19250.0,22750.0,22750.0,,19/03/2026
82
+ Nairobi,Nairobi,Kawangware,Dry maize,,5389.0,6500.0,6500.0,,18/03/2026
83
+ Nairobi,Nairobi,Kawangware,Beans (yellow-green),,16167.0,19500.0,19500.0,,18/03/2026
84
+ Nairobi,Nairobi,Kawangware,Red irish potato,,4311.2,5200.0,5200.0,,18/03/2026
85
+ Nairobi,Nairobi,Kawangware,Green grams,,18861.5,22750.0,22750.0,,18/03/2026
86
+ Nairobi,Nairobi,Kangemi Market,Dry maize,,5000.0,5000.0,5000.0,,18/03/2026
87
+ Nairobi,Nairobi,Kangemi Market,Beans (yellow-green),,15000.0,15000.0,15000.0,,18/03/2026
88
+ Nairobi,Nairobi,Kangemi Market,Red irish potato,,4000.0,4000.0,4000.0,,18/03/2026
89
+ Nairobi,Nairobi,Kangemi Market,Green grams,,17500.0,17500.0,17500.0,,18/03/2026
90
+ Nakuru,Nakuru,Molo,Dry maize,,4444.0,4444.0,4444.0,,27/05/2025
91
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,13332.0,13332.0,13332.0,,27/05/2025
92
+ Nakuru,Nakuru,Molo,Red irish potato,,3555.2,3555.2,3555.2,,27/05/2025
93
+ Nakuru,Nakuru,Molo,Green grams,,15554.0,15554.0,15554.0,,27/05/2025
94
+ Nakuru,Nakuru,Molo,Dry maize,,4444.0,4444.0,4444.0,,26/05/2025
95
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,13332.0,13332.0,13332.0,,26/05/2025
96
+ Nakuru,Nakuru,Molo,Red irish potato,,3555.2,3555.2,3555.2,,26/05/2025
97
+ Nakuru,Nakuru,Molo,Green grams,,15554.0,15554.0,15554.0,,26/05/2025
98
+ Nakuru,Nakuru,Molo,Dry maize,,4556.0,4556.0,4556.0,,24/05/2025
99
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,13668.0,13668.0,13668.0,,24/05/2025
100
+ Nakuru,Nakuru,Molo,Red irish potato,,3644.8,3644.8,3644.8,,24/05/2025
101
+ Nakuru,Nakuru,Molo,Green grams,,15946.0,15946.0,15946.0,,24/05/2025
102
+ Nakuru,Nakuru,Molo,Dry maize,,4556.0,4556.0,4556.0,,23/05/2025
103
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,13668.0,13668.0,13668.0,,23/05/2025
104
+ Nakuru,Nakuru,Molo,Red irish potato,,3644.8,3644.8,3644.8,,23/05/2025
105
+ Nakuru,Nakuru,Molo,Green grams,,15946.0,15946.0,15946.0,,23/05/2025
106
+ Nakuru,Nakuru,Molo,Dry maize,,4556.0,4556.0,4556.0,,22/05/2025
107
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,13668.0,13668.0,13668.0,,22/05/2025
108
+ Nakuru,Nakuru,Molo,Red irish potato,,3644.8,3644.8,3644.8,,22/05/2025
109
+ Nakuru,Nakuru,Molo,Green grams,,15946.0,15946.0,15946.0,,22/05/2025
110
+ Nakuru,Nakuru,Molo,Dry maize,,4333.0,4333.0,4333.0,,21/05/2025
111
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,12999.0,12999.0,12999.0,,21/05/2025
112
+ Nakuru,Nakuru,Molo,Red irish potato,,3466.4,3466.4,3466.4,,21/05/2025
113
+ Nakuru,Nakuru,Molo,Green grams,,15165.5,15165.5,15165.5,,21/05/2025
114
+ Nakuru,Nakuru,Molo,Dry maize,,4444.0,4444.0,4444.0,,20/05/2025
115
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,13332.0,13332.0,13332.0,,20/05/2025
116
+ Nakuru,Nakuru,Molo,Red irish potato,,3555.2,3555.2,3555.2,,20/05/2025
117
+ Nakuru,Nakuru,Molo,Green grams,,15554.0,15554.0,15554.0,,20/05/2025
118
+ Nakuru,Nakuru,Molo,Dry maize,,4222.0,4222.0,4222.0,,19/05/2025
119
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,12666.0,12666.0,12666.0,,19/05/2025
120
+ Nakuru,Nakuru,Molo,Red irish potato,,3377.6,3377.6,3377.6,,19/05/2025
121
+ Nakuru,Nakuru,Molo,Green grams,,14777.0,14777.0,14777.0,,19/05/2025
122
+ Nakuru,Nakuru,Molo,Dry maize,,3778.0,3778.0,3778.0,,17/05/2025
123
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,11334.0,11334.0,11334.0,,17/05/2025
124
+ Nakuru,Nakuru,Molo,Red irish potato,,3022.4,3022.4,3022.4,,17/05/2025
125
+ Nakuru,Nakuru,Molo,Green grams,,13223.0,13223.0,13223.0,,17/05/2025
126
+ Nakuru,Nakuru,Molo,Dry maize,,3778.0,3778.0,3778.0,,16/05/2025
127
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,11334.0,11334.0,11334.0,,16/05/2025
128
+ Nakuru,Nakuru,Molo,Red irish potato,,3022.4,3022.4,3022.4,,16/05/2025
129
+ Nakuru,Nakuru,Molo,Green grams,,13223.0,13223.0,13223.0,,16/05/2025
130
+ Nakuru,Nakuru,Molo,Dry maize,,3778.0,3778.0,3778.0,,15/05/2025
131
+ Nakuru,Nakuru,Molo,Beans (yellow-green),,11334.0,11334.0,11334.0,,15/05/2025
132
+ Nakuru,Nakuru,Molo,Red irish potato,,3022.4,3022.4,3022.4,,15/05/2025
133
+ Nakuru,Nakuru,Molo,Green grams,,13223.0,13223.0,13223.0,,15/05/2025
farmers_kenya.json ADDED
@@ -0,0 +1,1302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "farmer_id": "FMR-K0001",
4
+ "name": "Mogaka Okoth",
5
+ "location_name": "Kisumu",
6
+ "latitude": -0.2377,
7
+ "longitude": 34.6848,
8
+ "primary_commodity": "MZE-DRY",
9
+ "quantity_quintals": 9.9,
10
+ "has_storage": false,
11
+ "distance_to_market_km": 4.5,
12
+ "nearest_market_id": "MKT-KIB",
13
+ "notes": "Smallholder dry maize farmer in Kisumu. 4.5 km to Kibuye; no storage; must sell at harvest."
14
+ },
15
+ {
16
+ "farmer_id": "FMR-K0002",
17
+ "name": "Juma Odhiambo",
18
+ "location_name": "Kisumu",
19
+ "latitude": -0.0681,
20
+ "longitude": 34.6118,
21
+ "primary_commodity": "MZE-DRY",
22
+ "quantity_quintals": 12.7,
23
+ "has_storage": true,
24
+ "distance_to_market_km": 18.2,
25
+ "nearest_market_id": "MKT-KIB",
26
+ "notes": "Smallholder dry maize farmer in Kisumu. 18.2 km to Kibuye; has dry storage shed."
27
+ },
28
+ {
29
+ "farmer_id": "FMR-K0003",
30
+ "name": "Owino Muriuki",
31
+ "location_name": "Kisumu",
32
+ "latitude": -0.1193,
33
+ "longitude": 34.7371,
34
+ "primary_commodity": "BEN-YGN",
35
+ "quantity_quintals": 33.3,
36
+ "has_storage": false,
37
+ "distance_to_market_km": 1.2,
38
+ "nearest_market_id": "MKT-KIB",
39
+ "notes": "Smallholder beans farmer in Kisumu. 1.2 km to Kibuye; no storage; must sell at harvest."
40
+ },
41
+ {
42
+ "farmer_id": "FMR-K0004",
43
+ "name": "Jepkorir Kosgei",
44
+ "location_name": "Kisumu",
45
+ "latitude": -0.1618,
46
+ "longitude": 34.6669,
47
+ "primary_commodity": "BEN-YGN",
48
+ "quantity_quintals": 8.6,
49
+ "has_storage": true,
50
+ "distance_to_market_km": 13.9,
51
+ "nearest_market_id": "MKT-KIB",
52
+ "notes": "Smallholder beans farmer in Kisumu. 13.9 km to Kibuye; has dry storage shed."
53
+ },
54
+ {
55
+ "farmer_id": "FMR-K0005",
56
+ "name": "Kaloki Mwikali",
57
+ "location_name": "Kisumu",
58
+ "latitude": -0.0031,
59
+ "longitude": 34.8212,
60
+ "primary_commodity": "GRG-KEN",
61
+ "quantity_quintals": 9.4,
62
+ "has_storage": true,
63
+ "distance_to_market_km": 32.4,
64
+ "nearest_market_id": "MKT-KIB",
65
+ "notes": "Smallholder green grams farmer in Kisumu. 32.4 km to Kibuye; has dry storage shed."
66
+ },
67
+ {
68
+ "farmer_id": "FMR-K0006",
69
+ "name": "Wanjala Kibet",
70
+ "location_name": "Kisumu",
71
+ "latitude": -0.072,
72
+ "longitude": 34.8137,
73
+ "primary_commodity": "MZE-DRY",
74
+ "quantity_quintals": 28.1,
75
+ "has_storage": false,
76
+ "distance_to_market_km": 27.3,
77
+ "nearest_market_id": "MKT-KIB",
78
+ "notes": "Smallholder dry maize farmer in Kisumu. 27.3 km to Kibuye; no storage; must sell at harvest."
79
+ },
80
+ {
81
+ "farmer_id": "FMR-K0007",
82
+ "name": "Okoth Ochieng",
83
+ "location_name": "Kisumu",
84
+ "latitude": -0.1312,
85
+ "longitude": 34.7383,
86
+ "primary_commodity": "BEN-YGN",
87
+ "quantity_quintals": 10.7,
88
+ "has_storage": false,
89
+ "distance_to_market_km": 13.1,
90
+ "nearest_market_id": "MKT-KIB",
91
+ "notes": "Smallholder beans farmer in Kisumu. 13.1 km to Kibuye; no storage; must sell at harvest."
92
+ },
93
+ {
94
+ "farmer_id": "FMR-K0008",
95
+ "name": "Kamau Wekesa",
96
+ "location_name": "Kisumu",
97
+ "latitude": -0.085,
98
+ "longitude": 34.6757,
99
+ "primary_commodity": "BEN-YGN",
100
+ "quantity_quintals": 18.3,
101
+ "has_storage": false,
102
+ "distance_to_market_km": 34.6,
103
+ "nearest_market_id": "MKT-KIB",
104
+ "notes": "Smallholder beans farmer in Kisumu. 34.6 km to Kibuye; no storage; must sell at harvest."
105
+ },
106
+ {
107
+ "farmer_id": "FMR-K0009",
108
+ "name": "Asha Nafula",
109
+ "location_name": "Kisumu",
110
+ "latitude": -0.0398,
111
+ "longitude": 34.8552,
112
+ "primary_commodity": "MZE-DRY",
113
+ "quantity_quintals": 13.0,
114
+ "has_storage": true,
115
+ "distance_to_market_km": 2.1,
116
+ "nearest_market_id": "MKT-KIB",
117
+ "notes": "Smallholder dry maize farmer in Kisumu. 2.1 km to Kibuye; has dry storage shed."
118
+ },
119
+ {
120
+ "farmer_id": "FMR-K0010",
121
+ "name": "Wafula Kinyanjui",
122
+ "location_name": "Kisumu",
123
+ "latitude": -0.1819,
124
+ "longitude": 34.8852,
125
+ "primary_commodity": "GRG-KEN",
126
+ "quantity_quintals": 16.0,
127
+ "has_storage": true,
128
+ "distance_to_market_km": 23.3,
129
+ "nearest_market_id": "MKT-KIB",
130
+ "notes": "Smallholder green grams farmer in Kisumu. 23.3 km to Kibuye; has dry storage shed."
131
+ },
132
+ {
133
+ "farmer_id": "FMR-K0011",
134
+ "name": "Korir Omondi",
135
+ "location_name": "Kisumu",
136
+ "latitude": -0.1657,
137
+ "longitude": 34.6763,
138
+ "primary_commodity": "GRG-KEN",
139
+ "quantity_quintals": 23.9,
140
+ "has_storage": true,
141
+ "distance_to_market_km": 26.4,
142
+ "nearest_market_id": "MKT-KIB",
143
+ "notes": "Smallholder green grams farmer in Kisumu. 26.4 km to Kibuye; has dry storage shed."
144
+ },
145
+ {
146
+ "farmer_id": "FMR-K0012",
147
+ "name": "Mutembei Juma",
148
+ "location_name": "Nairobi",
149
+ "latitude": -1.3335,
150
+ "longitude": 36.9762,
151
+ "primary_commodity": "BEN-YGN",
152
+ "quantity_quintals": 22.8,
153
+ "has_storage": true,
154
+ "distance_to_market_km": 4.1,
155
+ "nearest_market_id": "MKT-NWK",
156
+ "notes": "Smallholder beans farmer in Nairobi. 4.1 km to Nairobi Wakulima; has dry storage shed."
157
+ },
158
+ {
159
+ "farmer_id": "FMR-K0013",
160
+ "name": "Kinuthia Ouma",
161
+ "location_name": "Nairobi",
162
+ "latitude": -1.2539,
163
+ "longitude": 36.9146,
164
+ "primary_commodity": "GRG-KEN",
165
+ "quantity_quintals": 25.9,
166
+ "has_storage": false,
167
+ "distance_to_market_km": 14.1,
168
+ "nearest_market_id": "MKT-NWK",
169
+ "notes": "Smallholder green grams farmer in Nairobi. 14.1 km to Nairobi Wakulima; no storage; must sell at harvest."
170
+ },
171
+ {
172
+ "farmer_id": "FMR-K0014",
173
+ "name": "Kipkoech Mogaka",
174
+ "location_name": "Nairobi",
175
+ "latitude": -1.3667,
176
+ "longitude": 36.843,
177
+ "primary_commodity": "MZE-DRY",
178
+ "quantity_quintals": 28.8,
179
+ "has_storage": false,
180
+ "distance_to_market_km": 4.9,
181
+ "nearest_market_id": "MKT-NWK",
182
+ "notes": "Smallholder dry maize farmer in Nairobi. 4.9 km to Nairobi Wakulima; no storage; must sell at harvest."
183
+ },
184
+ {
185
+ "farmer_id": "FMR-K0015",
186
+ "name": "Wafula Kosgei",
187
+ "location_name": "Nairobi",
188
+ "latitude": -1.4086,
189
+ "longitude": 36.8074,
190
+ "primary_commodity": "GRG-KEN",
191
+ "quantity_quintals": 5.1,
192
+ "has_storage": false,
193
+ "distance_to_market_km": 25.6,
194
+ "nearest_market_id": "MKT-NWK",
195
+ "notes": "Smallholder green grams farmer in Nairobi. 25.6 km to Nairobi Wakulima; no storage; must sell at harvest."
196
+ },
197
+ {
198
+ "farmer_id": "FMR-K0016",
199
+ "name": "Halima Barasa",
200
+ "location_name": "Nairobi",
201
+ "latitude": -1.2898,
202
+ "longitude": 36.7089,
203
+ "primary_commodity": "POT-RIR",
204
+ "quantity_quintals": 34.5,
205
+ "has_storage": true,
206
+ "distance_to_market_km": 18.3,
207
+ "nearest_market_id": "MKT-NWK",
208
+ "notes": "Smallholder Irish potato farmer in Nairobi. 18.3 km to Nairobi Wakulima; has dry storage shed."
209
+ },
210
+ {
211
+ "farmer_id": "FMR-K0017",
212
+ "name": "Muthama Wafula",
213
+ "location_name": "Nairobi",
214
+ "latitude": -1.2803,
215
+ "longitude": 36.9106,
216
+ "primary_commodity": "MZE-DRY",
217
+ "quantity_quintals": 26.0,
218
+ "has_storage": true,
219
+ "distance_to_market_km": 17.6,
220
+ "nearest_market_id": "MKT-NWK",
221
+ "notes": "Smallholder dry maize farmer in Nairobi. 17.6 km to Nairobi Wakulima; has dry storage shed."
222
+ },
223
+ {
224
+ "farmer_id": "FMR-K0018",
225
+ "name": "Musyoka Kiprop",
226
+ "location_name": "Nairobi",
227
+ "latitude": -1.3703,
228
+ "longitude": 36.7493,
229
+ "primary_commodity": "MZE-DRY",
230
+ "quantity_quintals": 8.0,
231
+ "has_storage": true,
232
+ "distance_to_market_km": 17.5,
233
+ "nearest_market_id": "MKT-NWK",
234
+ "notes": "Smallholder dry maize farmer in Nairobi. 17.5 km to Nairobi Wakulima; has dry storage shed."
235
+ },
236
+ {
237
+ "farmer_id": "FMR-K0019",
238
+ "name": "Said Opiyo",
239
+ "location_name": "Nairobi",
240
+ "latitude": -1.4036,
241
+ "longitude": 36.8196,
242
+ "primary_commodity": "BEN-YGN",
243
+ "quantity_quintals": 14.3,
244
+ "has_storage": true,
245
+ "distance_to_market_km": 30.7,
246
+ "nearest_market_id": "MKT-NWK",
247
+ "notes": "Smallholder beans farmer in Nairobi. 30.7 km to Nairobi Wakulima; has dry storage shed."
248
+ },
249
+ {
250
+ "farmer_id": "FMR-K0020",
251
+ "name": "Ouma Muriuki",
252
+ "location_name": "Nairobi",
253
+ "latitude": -1.2155,
254
+ "longitude": 36.884,
255
+ "primary_commodity": "POT-RIR",
256
+ "quantity_quintals": 19.0,
257
+ "has_storage": true,
258
+ "distance_to_market_km": 23.8,
259
+ "nearest_market_id": "MKT-NWK",
260
+ "notes": "Smallholder Irish potato farmer in Nairobi. 23.8 km to Nairobi Wakulima; has dry storage shed."
261
+ },
262
+ {
263
+ "farmer_id": "FMR-K0021",
264
+ "name": "Hassan Abdalla",
265
+ "location_name": "Nairobi",
266
+ "latitude": -1.4058,
267
+ "longitude": 36.7444,
268
+ "primary_commodity": "POT-RIR",
269
+ "quantity_quintals": 5.7,
270
+ "has_storage": false,
271
+ "distance_to_market_km": 19.8,
272
+ "nearest_market_id": "MKT-NWK",
273
+ "notes": "Smallholder Irish potato farmer in Nairobi. 19.8 km to Nairobi Wakulima; no storage; must sell at harvest."
274
+ },
275
+ {
276
+ "farmer_id": "FMR-K0022",
277
+ "name": "Wanjiku Mbugua",
278
+ "location_name": "Nairobi",
279
+ "latitude": -1.2297,
280
+ "longitude": 36.6947,
281
+ "primary_commodity": "MZE-DRY",
282
+ "quantity_quintals": 36.7,
283
+ "has_storage": true,
284
+ "distance_to_market_km": 30.2,
285
+ "nearest_market_id": "MKT-NWK",
286
+ "notes": "Smallholder dry maize farmer in Nairobi. 30.2 km to Nairobi Wakulima; has dry storage shed."
287
+ },
288
+ {
289
+ "farmer_id": "FMR-K0023",
290
+ "name": "Nabwire Nzioka",
291
+ "location_name": "Nairobi",
292
+ "latitude": -1.2414,
293
+ "longitude": 36.7413,
294
+ "primary_commodity": "BEN-YGN",
295
+ "quantity_quintals": 30.3,
296
+ "has_storage": false,
297
+ "distance_to_market_km": 31.0,
298
+ "nearest_market_id": "MKT-NWK",
299
+ "notes": "Smallholder beans farmer in Nairobi. 31.0 km to Nairobi Wakulima; no storage; must sell at harvest."
300
+ },
301
+ {
302
+ "farmer_id": "FMR-K0024",
303
+ "name": "Nafula Mutembei",
304
+ "location_name": "Nairobi",
305
+ "latitude": -1.1999,
306
+ "longitude": 36.7341,
307
+ "primary_commodity": "MZE-DRY",
308
+ "quantity_quintals": 28.1,
309
+ "has_storage": true,
310
+ "distance_to_market_km": 13.0,
311
+ "nearest_market_id": "MKT-NWK",
312
+ "notes": "Smallholder dry maize farmer in Nairobi. 13.0 km to Nairobi Wakulima; has dry storage shed."
313
+ },
314
+ {
315
+ "farmer_id": "FMR-K0025",
316
+ "name": "Muthoni Ochieng",
317
+ "location_name": "Mombasa",
318
+ "latitude": -4.1595,
319
+ "longitude": 39.7561,
320
+ "primary_commodity": "MZE-DRY",
321
+ "quantity_quintals": 13.7,
322
+ "has_storage": true,
323
+ "distance_to_market_km": 7.5,
324
+ "nearest_market_id": "MKT-KON",
325
+ "notes": "Smallholder dry maize farmer in Mombasa. 7.5 km to Kongowea; has dry storage shed."
326
+ },
327
+ {
328
+ "farmer_id": "FMR-K0026",
329
+ "name": "Jepkorir Simiyu",
330
+ "location_name": "Mombasa",
331
+ "latitude": -4.0941,
332
+ "longitude": 39.6125,
333
+ "primary_commodity": "MZE-DRY",
334
+ "quantity_quintals": 20.5,
335
+ "has_storage": false,
336
+ "distance_to_market_km": 30.3,
337
+ "nearest_market_id": "MKT-KON",
338
+ "notes": "Smallholder dry maize farmer in Mombasa. 30.3 km to Kongowea; no storage; must sell at harvest."
339
+ },
340
+ {
341
+ "farmer_id": "FMR-K0027",
342
+ "name": "Muthoni Muriuki",
343
+ "location_name": "Mombasa",
344
+ "latitude": -3.9269,
345
+ "longitude": 39.8283,
346
+ "primary_commodity": "BEN-YGN",
347
+ "quantity_quintals": 10.8,
348
+ "has_storage": true,
349
+ "distance_to_market_km": 17.5,
350
+ "nearest_market_id": "MKT-KON",
351
+ "notes": "Smallholder beans farmer in Mombasa. 17.5 km to Kongowea; has dry storage shed."
352
+ },
353
+ {
354
+ "farmer_id": "FMR-K0028",
355
+ "name": "Kipchumba Ndungu",
356
+ "location_name": "Mombasa",
357
+ "latitude": -4.1283,
358
+ "longitude": 39.5382,
359
+ "primary_commodity": "GRG-KEN",
360
+ "quantity_quintals": 14.3,
361
+ "has_storage": false,
362
+ "distance_to_market_km": 27.7,
363
+ "nearest_market_id": "MKT-KON",
364
+ "notes": "Smallholder green grams farmer in Mombasa. 27.7 km to Kongowea; no storage; must sell at harvest."
365
+ },
366
+ {
367
+ "farmer_id": "FMR-K0029",
368
+ "name": "Jepkorir Kathambi",
369
+ "location_name": "Mombasa",
370
+ "latitude": -4.1313,
371
+ "longitude": 39.6266,
372
+ "primary_commodity": "MZE-DRY",
373
+ "quantity_quintals": 25.3,
374
+ "has_storage": false,
375
+ "distance_to_market_km": 19.4,
376
+ "nearest_market_id": "MKT-KON",
377
+ "notes": "Smallholder dry maize farmer in Mombasa. 19.4 km to Kongowea; no storage; must sell at harvest."
378
+ },
379
+ {
380
+ "farmer_id": "FMR-K0030",
381
+ "name": "Nyokabi Gitau",
382
+ "location_name": "Mombasa",
383
+ "latitude": -4.0024,
384
+ "longitude": 39.6885,
385
+ "primary_commodity": "BEN-YGN",
386
+ "quantity_quintals": 7.0,
387
+ "has_storage": false,
388
+ "distance_to_market_km": 18.3,
389
+ "nearest_market_id": "MKT-KON",
390
+ "notes": "Smallholder beans farmer in Mombasa. 18.3 km to Kongowea; no storage; must sell at harvest."
391
+ },
392
+ {
393
+ "farmer_id": "FMR-K0031",
394
+ "name": "Njenga Kinyanjui",
395
+ "location_name": "Mombasa",
396
+ "latitude": -3.9751,
397
+ "longitude": 39.6082,
398
+ "primary_commodity": "MZE-DRY",
399
+ "quantity_quintals": 38.0,
400
+ "has_storage": false,
401
+ "distance_to_market_km": 20.4,
402
+ "nearest_market_id": "MKT-KON",
403
+ "notes": "Smallholder dry maize farmer in Mombasa. 20.4 km to Kongowea; no storage; must sell at harvest."
404
+ },
405
+ {
406
+ "farmer_id": "FMR-K0032",
407
+ "name": "Wangari Otieno",
408
+ "location_name": "Mombasa",
409
+ "latitude": -4.0519,
410
+ "longitude": 39.7127,
411
+ "primary_commodity": "POT-RIR",
412
+ "quantity_quintals": 37.7,
413
+ "has_storage": false,
414
+ "distance_to_market_km": 7.9,
415
+ "nearest_market_id": "MKT-KON",
416
+ "notes": "Smallholder Irish potato farmer in Mombasa. 7.9 km to Kongowea; no storage; must sell at harvest."
417
+ },
418
+ {
419
+ "farmer_id": "FMR-K0033",
420
+ "name": "Nabwire Mwikali",
421
+ "location_name": "Uasin Gishu",
422
+ "latitude": 0.483,
423
+ "longitude": 35.3213,
424
+ "primary_commodity": "BEN-YGN",
425
+ "quantity_quintals": 21.0,
426
+ "has_storage": false,
427
+ "distance_to_market_km": 32.6,
428
+ "nearest_market_id": "MKT-ELD",
429
+ "notes": "Smallholder beans farmer in Uasin Gishu. 32.6 km to Eldoret Main; no storage; must sell at harvest."
430
+ },
431
+ {
432
+ "farmer_id": "FMR-K0034",
433
+ "name": "Njeri Mwenda",
434
+ "location_name": "Uasin Gishu",
435
+ "latitude": 0.5506,
436
+ "longitude": 35.2887,
437
+ "primary_commodity": "MZE-DRY",
438
+ "quantity_quintals": 7.6,
439
+ "has_storage": true,
440
+ "distance_to_market_km": 8.2,
441
+ "nearest_market_id": "MKT-ELD",
442
+ "notes": "Smallholder dry maize farmer in Uasin Gishu. 8.2 km to Eldoret Main; has dry storage shed."
443
+ },
444
+ {
445
+ "farmer_id": "FMR-K0035",
446
+ "name": "Kaloki Kinyanjui",
447
+ "location_name": "Uasin Gishu",
448
+ "latitude": 0.6281,
449
+ "longitude": 35.2307,
450
+ "primary_commodity": "MZE-DRY",
451
+ "quantity_quintals": 20.3,
452
+ "has_storage": true,
453
+ "distance_to_market_km": 19.5,
454
+ "nearest_market_id": "MKT-ELD",
455
+ "notes": "Smallholder dry maize farmer in Uasin Gishu. 19.5 km to Eldoret Main; has dry storage shed."
456
+ },
457
+ {
458
+ "farmer_id": "FMR-K0036",
459
+ "name": "Salim Mwangi",
460
+ "location_name": "Uasin Gishu",
461
+ "latitude": 0.5647,
462
+ "longitude": 35.2862,
463
+ "primary_commodity": "POT-RIR",
464
+ "quantity_quintals": 8.6,
465
+ "has_storage": true,
466
+ "distance_to_market_km": 30.9,
467
+ "nearest_market_id": "MKT-ELD",
468
+ "notes": "Smallholder Irish potato farmer in Uasin Gishu. 30.9 km to Eldoret Main; has dry storage shed."
469
+ },
470
+ {
471
+ "farmer_id": "FMR-K0037",
472
+ "name": "Githinji Ouma",
473
+ "location_name": "Uasin Gishu",
474
+ "latitude": 0.446,
475
+ "longitude": 35.3012,
476
+ "primary_commodity": "POT-RIR",
477
+ "quantity_quintals": 17.0,
478
+ "has_storage": false,
479
+ "distance_to_market_km": 24.4,
480
+ "nearest_market_id": "MKT-ELD",
481
+ "notes": "Smallholder Irish potato farmer in Uasin Gishu. 24.4 km to Eldoret Main; no storage; must sell at harvest."
482
+ },
483
+ {
484
+ "farmer_id": "FMR-K0038",
485
+ "name": "Halima Kathambi",
486
+ "location_name": "Uasin Gishu",
487
+ "latitude": 0.4396,
488
+ "longitude": 35.3922,
489
+ "primary_commodity": "MZE-DRY",
490
+ "quantity_quintals": 8.2,
491
+ "has_storage": true,
492
+ "distance_to_market_km": 15.4,
493
+ "nearest_market_id": "MKT-ELD",
494
+ "notes": "Smallholder dry maize farmer in Uasin Gishu. 15.4 km to Eldoret Main; has dry storage shed."
495
+ },
496
+ {
497
+ "farmer_id": "FMR-K0039",
498
+ "name": "Wanjiku Chebet",
499
+ "location_name": "Uasin Gishu",
500
+ "latitude": 0.5956,
501
+ "longitude": 35.3109,
502
+ "primary_commodity": "BEN-YGN",
503
+ "quantity_quintals": 10.7,
504
+ "has_storage": false,
505
+ "distance_to_market_km": 16.0,
506
+ "nearest_market_id": "MKT-ELD",
507
+ "notes": "Smallholder beans farmer in Uasin Gishu. 16.0 km to Eldoret Main; no storage; must sell at harvest."
508
+ },
509
+ {
510
+ "farmer_id": "FMR-K0040",
511
+ "name": "Asha Mwangi",
512
+ "location_name": "Uasin Gishu",
513
+ "latitude": 0.3979,
514
+ "longitude": 35.4033,
515
+ "primary_commodity": "POT-RIR",
516
+ "quantity_quintals": 36.6,
517
+ "has_storage": false,
518
+ "distance_to_market_km": 19.6,
519
+ "nearest_market_id": "MKT-ELD",
520
+ "notes": "Smallholder Irish potato farmer in Uasin Gishu. 19.6 km to Eldoret Main; no storage; must sell at harvest."
521
+ },
522
+ {
523
+ "farmer_id": "FMR-K0041",
524
+ "name": "Mutembei Omondi",
525
+ "location_name": "Uasin Gishu",
526
+ "latitude": 0.4932,
527
+ "longitude": 35.1323,
528
+ "primary_commodity": "BEN-YGN",
529
+ "quantity_quintals": 36.5,
530
+ "has_storage": false,
531
+ "distance_to_market_km": 28.1,
532
+ "nearest_market_id": "MKT-ELD",
533
+ "notes": "Smallholder beans farmer in Uasin Gishu. 28.1 km to Eldoret Main; no storage; must sell at harvest."
534
+ },
535
+ {
536
+ "farmer_id": "FMR-K0042",
537
+ "name": "Mwende Nabwire",
538
+ "location_name": "Uasin Gishu",
539
+ "latitude": 0.5689,
540
+ "longitude": 35.3199,
541
+ "primary_commodity": "BEN-YGN",
542
+ "quantity_quintals": 32.3,
543
+ "has_storage": true,
544
+ "distance_to_market_km": 31.1,
545
+ "nearest_market_id": "MKT-ELD",
546
+ "notes": "Smallholder beans farmer in Uasin Gishu. 31.1 km to Eldoret Main; has dry storage shed."
547
+ },
548
+ {
549
+ "farmer_id": "FMR-K0043",
550
+ "name": "Moraa Ouma",
551
+ "location_name": "Uasin Gishu",
552
+ "latitude": 0.642,
553
+ "longitude": 35.1908,
554
+ "primary_commodity": "MZE-DRY",
555
+ "quantity_quintals": 39.2,
556
+ "has_storage": false,
557
+ "distance_to_market_km": 28.6,
558
+ "nearest_market_id": "MKT-ELD",
559
+ "notes": "Smallholder dry maize farmer in Uasin Gishu. 28.6 km to Eldoret Main; no storage; must sell at harvest."
560
+ },
561
+ {
562
+ "farmer_id": "FMR-K0044",
563
+ "name": "Mumbi Barasa",
564
+ "location_name": "Uasin Gishu",
565
+ "latitude": 0.5853,
566
+ "longitude": 35.2195,
567
+ "primary_commodity": "BEN-YGN",
568
+ "quantity_quintals": 33.1,
569
+ "has_storage": false,
570
+ "distance_to_market_km": 30.4,
571
+ "nearest_market_id": "MKT-ELD",
572
+ "notes": "Smallholder beans farmer in Uasin Gishu. 30.4 km to Eldoret Main; no storage; must sell at harvest."
573
+ },
574
+ {
575
+ "farmer_id": "FMR-K0045",
576
+ "name": "Wafula Owino",
577
+ "location_name": "Uasin Gishu",
578
+ "latitude": 0.4791,
579
+ "longitude": 35.1314,
580
+ "primary_commodity": "BEN-YGN",
581
+ "quantity_quintals": 12.8,
582
+ "has_storage": false,
583
+ "distance_to_market_km": 28.8,
584
+ "nearest_market_id": "MKT-ELD",
585
+ "notes": "Smallholder beans farmer in Uasin Gishu. 28.8 km to Eldoret Main; no storage; must sell at harvest."
586
+ },
587
+ {
588
+ "farmer_id": "FMR-K0046",
589
+ "name": "Shikuku Mutua",
590
+ "location_name": "Uasin Gishu",
591
+ "latitude": 0.4312,
592
+ "longitude": 35.3178,
593
+ "primary_commodity": "BEN-YGN",
594
+ "quantity_quintals": 16.5,
595
+ "has_storage": false,
596
+ "distance_to_market_km": 30.4,
597
+ "nearest_market_id": "MKT-ELD",
598
+ "notes": "Smallholder beans farmer in Uasin Gishu. 30.4 km to Eldoret Main; no storage; must sell at harvest."
599
+ },
600
+ {
601
+ "farmer_id": "FMR-K0047",
602
+ "name": "Barasa Korir",
603
+ "location_name": "Nakuru",
604
+ "latitude": -0.2607,
605
+ "longitude": 36.0499,
606
+ "primary_commodity": "POT-RIR",
607
+ "quantity_quintals": 16.6,
608
+ "has_storage": false,
609
+ "distance_to_market_km": 1.9,
610
+ "nearest_market_id": "MKT-NAK",
611
+ "notes": "Smallholder Irish potato farmer in Nakuru. 1.9 km to Nakuru Wakulima; no storage; must sell at harvest."
612
+ },
613
+ {
614
+ "farmer_id": "FMR-K0048",
615
+ "name": "Wekesa Barasa",
616
+ "location_name": "Nakuru",
617
+ "latitude": -0.2789,
618
+ "longitude": 36.2251,
619
+ "primary_commodity": "MZE-DRY",
620
+ "quantity_quintals": 8.8,
621
+ "has_storage": false,
622
+ "distance_to_market_km": 15.8,
623
+ "nearest_market_id": "MKT-NAK",
624
+ "notes": "Smallholder dry maize farmer in Nakuru. 15.8 km to Nakuru Wakulima; no storage; must sell at harvest."
625
+ },
626
+ {
627
+ "farmer_id": "FMR-K0049",
628
+ "name": "Mutua Mohamed",
629
+ "location_name": "Nakuru",
630
+ "latitude": -0.2712,
631
+ "longitude": 36.0834,
632
+ "primary_commodity": "BEN-YGN",
633
+ "quantity_quintals": 36.5,
634
+ "has_storage": true,
635
+ "distance_to_market_km": 7.5,
636
+ "nearest_market_id": "MKT-NAK",
637
+ "notes": "Smallholder beans farmer in Nakuru. 7.5 km to Nakuru Wakulima; has dry storage shed."
638
+ },
639
+ {
640
+ "farmer_id": "FMR-K0050",
641
+ "name": "Chepngetich Kamau",
642
+ "location_name": "Nakuru",
643
+ "latitude": -0.2971,
644
+ "longitude": 36.172,
645
+ "primary_commodity": "POT-RIR",
646
+ "quantity_quintals": 30.2,
647
+ "has_storage": false,
648
+ "distance_to_market_km": 26.2,
649
+ "nearest_market_id": "MKT-NAK",
650
+ "notes": "Smallholder Irish potato farmer in Nakuru. 26.2 km to Nakuru Wakulima; no storage; must sell at harvest."
651
+ },
652
+ {
653
+ "farmer_id": "FMR-K0051",
654
+ "name": "Musyoka Mohamed",
655
+ "location_name": "Nakuru",
656
+ "latitude": -0.4321,
657
+ "longitude": 36.1293,
658
+ "primary_commodity": "BEN-YGN",
659
+ "quantity_quintals": 26.8,
660
+ "has_storage": true,
661
+ "distance_to_market_km": 23.6,
662
+ "nearest_market_id": "MKT-NAK",
663
+ "notes": "Smallholder beans farmer in Nakuru. 23.6 km to Nakuru Wakulima; has dry storage shed."
664
+ },
665
+ {
666
+ "farmer_id": "FMR-K0052",
667
+ "name": "Makokha Kwamboka",
668
+ "location_name": "Nakuru",
669
+ "latitude": -0.3603,
670
+ "longitude": 36.0525,
671
+ "primary_commodity": "BEN-YGN",
672
+ "quantity_quintals": 29.4,
673
+ "has_storage": true,
674
+ "distance_to_market_km": 19.9,
675
+ "nearest_market_id": "MKT-NAK",
676
+ "notes": "Smallholder beans farmer in Nakuru. 19.9 km to Nakuru Wakulima; has dry storage shed."
677
+ },
678
+ {
679
+ "farmer_id": "FMR-K0053",
680
+ "name": "Nzioka Barasa",
681
+ "location_name": "Nakuru",
682
+ "latitude": -0.2684,
683
+ "longitude": 36.0203,
684
+ "primary_commodity": "POT-RIR",
685
+ "quantity_quintals": 34.2,
686
+ "has_storage": true,
687
+ "distance_to_market_km": 11.3,
688
+ "nearest_market_id": "MKT-NAK",
689
+ "notes": "Smallholder Irish potato farmer in Nakuru. 11.3 km to Nakuru Wakulima; has dry storage shed."
690
+ },
691
+ {
692
+ "farmer_id": "FMR-K0054",
693
+ "name": "Mutembei Chepkoech",
694
+ "location_name": "Nakuru",
695
+ "latitude": -0.3136,
696
+ "longitude": 36.0626,
697
+ "primary_commodity": "MZE-DRY",
698
+ "quantity_quintals": 22.9,
699
+ "has_storage": false,
700
+ "distance_to_market_km": 28.0,
701
+ "nearest_market_id": "MKT-NAK",
702
+ "notes": "Smallholder dry maize farmer in Nakuru. 28.0 km to Nakuru Wakulima; no storage; must sell at harvest."
703
+ },
704
+ {
705
+ "farmer_id": "FMR-K0055",
706
+ "name": "Awino Otieno",
707
+ "location_name": "Nakuru",
708
+ "latitude": -0.368,
709
+ "longitude": 36.1292,
710
+ "primary_commodity": "POT-RIR",
711
+ "quantity_quintals": 16.7,
712
+ "has_storage": false,
713
+ "distance_to_market_km": 28.8,
714
+ "nearest_market_id": "MKT-NAK",
715
+ "notes": "Smallholder Irish potato farmer in Nakuru. 28.8 km to Nakuru Wakulima; no storage; must sell at harvest."
716
+ },
717
+ {
718
+ "farmer_id": "FMR-K0056",
719
+ "name": "Shikuku Nafula",
720
+ "location_name": "Nakuru",
721
+ "latitude": -0.2111,
722
+ "longitude": 35.9742,
723
+ "primary_commodity": "MZE-DRY",
724
+ "quantity_quintals": 13.6,
725
+ "has_storage": false,
726
+ "distance_to_market_km": 17.2,
727
+ "nearest_market_id": "MKT-NAK",
728
+ "notes": "Smallholder dry maize farmer in Nakuru. 17.2 km to Nakuru Wakulima; no storage; must sell at harvest."
729
+ },
730
+ {
731
+ "farmer_id": "FMR-K0057",
732
+ "name": "Kamau Mwenda",
733
+ "location_name": "Nakuru",
734
+ "latitude": -0.3288,
735
+ "longitude": 36.1189,
736
+ "primary_commodity": "MZE-DRY",
737
+ "quantity_quintals": 30.1,
738
+ "has_storage": true,
739
+ "distance_to_market_km": 14.1,
740
+ "nearest_market_id": "MKT-NAK",
741
+ "notes": "Smallholder dry maize farmer in Nakuru. 14.1 km to Nakuru Wakulima; has dry storage shed."
742
+ },
743
+ {
744
+ "farmer_id": "FMR-K0058",
745
+ "name": "Adhiambo Kamau",
746
+ "location_name": "Nakuru",
747
+ "latitude": -0.1853,
748
+ "longitude": 36.1881,
749
+ "primary_commodity": "MZE-DRY",
750
+ "quantity_quintals": 32.2,
751
+ "has_storage": false,
752
+ "distance_to_market_km": 8.4,
753
+ "nearest_market_id": "MKT-NAK",
754
+ "notes": "Smallholder dry maize farmer in Nakuru. 8.4 km to Nakuru Wakulima; no storage; must sell at harvest."
755
+ },
756
+ {
757
+ "farmer_id": "FMR-K0059",
758
+ "name": "Hassan Kinoti",
759
+ "location_name": "Nakuru",
760
+ "latitude": -0.438,
761
+ "longitude": 36.0048,
762
+ "primary_commodity": "MZE-DRY",
763
+ "quantity_quintals": 21.0,
764
+ "has_storage": false,
765
+ "distance_to_market_km": 28.2,
766
+ "nearest_market_id": "MKT-NAK",
767
+ "notes": "Smallholder dry maize farmer in Nakuru. 28.2 km to Nakuru Wakulima; no storage; must sell at harvest."
768
+ },
769
+ {
770
+ "farmer_id": "FMR-K0060",
771
+ "name": "Asha Kipchumba",
772
+ "location_name": "Kajiado",
773
+ "latitude": -1.3397,
774
+ "longitude": 37.0761,
775
+ "primary_commodity": "GRG-KEN",
776
+ "quantity_quintals": 33.5,
777
+ "has_storage": true,
778
+ "distance_to_market_km": 31.3,
779
+ "nearest_market_id": "MKT-KTG",
780
+ "notes": "Smallholder green grams farmer in Kajiado. 31.3 km to Kitengela; has dry storage shed."
781
+ },
782
+ {
783
+ "farmer_id": "FMR-K0061",
784
+ "name": "Rehema Abdalla",
785
+ "location_name": "Kajiado",
786
+ "latitude": -1.3555,
787
+ "longitude": 37.0318,
788
+ "primary_commodity": "BEN-YGN",
789
+ "quantity_quintals": 20.8,
790
+ "has_storage": false,
791
+ "distance_to_market_km": 26.6,
792
+ "nearest_market_id": "MKT-KTG",
793
+ "notes": "Smallholder beans farmer in Kajiado. 26.6 km to Kitengela; no storage; must sell at harvest."
794
+ },
795
+ {
796
+ "farmer_id": "FMR-K0062",
797
+ "name": "Barasa Nyaboke",
798
+ "location_name": "Kajiado",
799
+ "latitude": -1.4793,
800
+ "longitude": 36.8805,
801
+ "primary_commodity": "BEN-YGN",
802
+ "quantity_quintals": 7.7,
803
+ "has_storage": true,
804
+ "distance_to_market_km": 10.7,
805
+ "nearest_market_id": "MKT-KTG",
806
+ "notes": "Smallholder beans farmer in Kajiado. 10.7 km to Kitengela; has dry storage shed."
807
+ },
808
+ {
809
+ "farmer_id": "FMR-K0063",
810
+ "name": "Mutua Muriuki",
811
+ "location_name": "Kajiado",
812
+ "latitude": -1.6005,
813
+ "longitude": 36.8539,
814
+ "primary_commodity": "BEN-YGN",
815
+ "quantity_quintals": 29.3,
816
+ "has_storage": true,
817
+ "distance_to_market_km": 25.0,
818
+ "nearest_market_id": "MKT-KTG",
819
+ "notes": "Smallholder beans farmer in Kajiado. 25.0 km to Kitengela; has dry storage shed."
820
+ },
821
+ {
822
+ "farmer_id": "FMR-K0064",
823
+ "name": "Chepkoech Chebet",
824
+ "location_name": "Kajiado",
825
+ "latitude": -1.4619,
826
+ "longitude": 36.9334,
827
+ "primary_commodity": "MZE-DRY",
828
+ "quantity_quintals": 34.2,
829
+ "has_storage": false,
830
+ "distance_to_market_km": 14.2,
831
+ "nearest_market_id": "MKT-KTG",
832
+ "notes": "Smallholder dry maize farmer in Kajiado. 14.2 km to Kitengela; no storage; must sell at harvest."
833
+ },
834
+ {
835
+ "farmer_id": "FMR-K0065",
836
+ "name": "Wambui Jepkorir",
837
+ "location_name": "Kajiado",
838
+ "latitude": -1.4816,
839
+ "longitude": 37.0915,
840
+ "primary_commodity": "BEN-YGN",
841
+ "quantity_quintals": 31.4,
842
+ "has_storage": false,
843
+ "distance_to_market_km": 30.0,
844
+ "nearest_market_id": "MKT-KTG",
845
+ "notes": "Smallholder beans farmer in Kajiado. 30.0 km to Kitengela; no storage; must sell at harvest."
846
+ },
847
+ {
848
+ "farmer_id": "FMR-K0066",
849
+ "name": "Chebet Njiru",
850
+ "location_name": "Kajiado",
851
+ "latitude": -1.4004,
852
+ "longitude": 36.9725,
853
+ "primary_commodity": "GRG-KEN",
854
+ "quantity_quintals": 36.4,
855
+ "has_storage": true,
856
+ "distance_to_market_km": 17.6,
857
+ "nearest_market_id": "MKT-KTG",
858
+ "notes": "Smallholder green grams farmer in Kajiado. 17.6 km to Kitengela; has dry storage shed."
859
+ },
860
+ {
861
+ "farmer_id": "FMR-K0067",
862
+ "name": "Amina Njoroge",
863
+ "location_name": "Kajiado",
864
+ "latitude": -1.508,
865
+ "longitude": 37.0094,
866
+ "primary_commodity": "BEN-YGN",
867
+ "quantity_quintals": 30.3,
868
+ "has_storage": false,
869
+ "distance_to_market_km": 29.6,
870
+ "nearest_market_id": "MKT-KTG",
871
+ "notes": "Smallholder beans farmer in Kajiado. 29.6 km to Kitengela; no storage; must sell at harvest."
872
+ },
873
+ {
874
+ "farmer_id": "FMR-K0068",
875
+ "name": "Moraa Njiru",
876
+ "location_name": "Nairobi",
877
+ "latitude": -1.4099,
878
+ "longitude": 36.7144,
879
+ "primary_commodity": "POT-RIR",
880
+ "quantity_quintals": 28.2,
881
+ "has_storage": true,
882
+ "distance_to_market_km": 3.9,
883
+ "nearest_market_id": "MKT-KGM",
884
+ "notes": "Smallholder Irish potato farmer in Nairobi. 3.9 km to Kangemi Market; has dry storage shed."
885
+ },
886
+ {
887
+ "farmer_id": "FMR-K0069",
888
+ "name": "Kipkoech Simiyu",
889
+ "location_name": "Nairobi",
890
+ "latitude": -1.4029,
891
+ "longitude": 36.7099,
892
+ "primary_commodity": "MZE-DRY",
893
+ "quantity_quintals": 20.9,
894
+ "has_storage": false,
895
+ "distance_to_market_km": 12.5,
896
+ "nearest_market_id": "MKT-KGM",
897
+ "notes": "Smallholder dry maize farmer in Nairobi. 12.5 km to Kangemi Market; no storage; must sell at harvest."
898
+ },
899
+ {
900
+ "farmer_id": "FMR-K0070",
901
+ "name": "Barasa Rehema",
902
+ "location_name": "Nairobi",
903
+ "latitude": -1.3423,
904
+ "longitude": 36.6208,
905
+ "primary_commodity": "MZE-DRY",
906
+ "quantity_quintals": 31.2,
907
+ "has_storage": false,
908
+ "distance_to_market_km": 2.8,
909
+ "nearest_market_id": "MKT-KGM",
910
+ "notes": "Smallholder dry maize farmer in Nairobi. 2.8 km to Kangemi Market; no storage; must sell at harvest."
911
+ },
912
+ {
913
+ "farmer_id": "FMR-K0071",
914
+ "name": "Onyango Kinyanjui",
915
+ "location_name": "Nairobi",
916
+ "latitude": -1.1836,
917
+ "longitude": 36.7917,
918
+ "primary_commodity": "MZE-DRY",
919
+ "quantity_quintals": 38.2,
920
+ "has_storage": true,
921
+ "distance_to_market_km": 7.8,
922
+ "nearest_market_id": "MKT-KGM",
923
+ "notes": "Smallholder dry maize farmer in Nairobi. 7.8 km to Kangemi Market; has dry storage shed."
924
+ },
925
+ {
926
+ "farmer_id": "FMR-K0072",
927
+ "name": "Auma Musyoka",
928
+ "location_name": "Nairobi",
929
+ "latitude": -1.3801,
930
+ "longitude": 36.797,
931
+ "primary_commodity": "POT-RIR",
932
+ "quantity_quintals": 38.2,
933
+ "has_storage": true,
934
+ "distance_to_market_km": 16.8,
935
+ "nearest_market_id": "MKT-KGM",
936
+ "notes": "Smallholder Irish potato farmer in Nairobi. 16.8 km to Kangemi Market; has dry storage shed."
937
+ },
938
+ {
939
+ "farmer_id": "FMR-K0073",
940
+ "name": "Muthama Wekesa",
941
+ "location_name": "Nairobi",
942
+ "latitude": -1.2362,
943
+ "longitude": 36.8853,
944
+ "primary_commodity": "POT-RIR",
945
+ "quantity_quintals": 9.0,
946
+ "has_storage": false,
947
+ "distance_to_market_km": 28.9,
948
+ "nearest_market_id": "MKT-KGM",
949
+ "notes": "Smallholder Irish potato farmer in Nairobi. 28.9 km to Kangemi Market; no storage; must sell at harvest."
950
+ },
951
+ {
952
+ "farmer_id": "FMR-K0074",
953
+ "name": "Githinji Njoroge",
954
+ "location_name": "Nairobi",
955
+ "latitude": -1.1393,
956
+ "longitude": 36.7689,
957
+ "primary_commodity": "BEN-YGN",
958
+ "quantity_quintals": 18.9,
959
+ "has_storage": true,
960
+ "distance_to_market_km": 25.3,
961
+ "nearest_market_id": "MKT-KGM",
962
+ "notes": "Smallholder beans farmer in Nairobi. 25.3 km to Kangemi Market; has dry storage shed."
963
+ },
964
+ {
965
+ "farmer_id": "FMR-K0075",
966
+ "name": "Nyaboke Mueni",
967
+ "location_name": "Nairobi",
968
+ "latitude": -1.3874,
969
+ "longitude": 36.8279,
970
+ "primary_commodity": "POT-RIR",
971
+ "quantity_quintals": 26.0,
972
+ "has_storage": false,
973
+ "distance_to_market_km": 5.1,
974
+ "nearest_market_id": "MKT-KGM",
975
+ "notes": "Smallholder Irish potato farmer in Nairobi. 5.1 km to Kangemi Market; no storage; must sell at harvest."
976
+ },
977
+ {
978
+ "farmer_id": "FMR-K0076",
979
+ "name": "Wangari Korir",
980
+ "location_name": "Nairobi",
981
+ "latitude": -1.2582,
982
+ "longitude": 36.7947,
983
+ "primary_commodity": "MZE-DRY",
984
+ "quantity_quintals": 22.7,
985
+ "has_storage": false,
986
+ "distance_to_market_km": 12.6,
987
+ "nearest_market_id": "MKT-KGM",
988
+ "notes": "Smallholder dry maize farmer in Nairobi. 12.6 km to Kangemi Market; no storage; must sell at harvest."
989
+ },
990
+ {
991
+ "farmer_id": "FMR-K0077",
992
+ "name": "Amina Owino",
993
+ "location_name": "Nyeri",
994
+ "latitude": -0.5032,
995
+ "longitude": 37.092,
996
+ "primary_commodity": "BEN-YGN",
997
+ "quantity_quintals": 29.8,
998
+ "has_storage": false,
999
+ "distance_to_market_km": 15.8,
1000
+ "nearest_market_id": "MKT-KRT",
1001
+ "notes": "Smallholder beans farmer in Nyeri. 15.8 km to Karatina; no storage; must sell at harvest."
1002
+ },
1003
+ {
1004
+ "farmer_id": "FMR-K0078",
1005
+ "name": "Wafula Njiru",
1006
+ "location_name": "Nyeri",
1007
+ "latitude": -0.4009,
1008
+ "longitude": 37.1228,
1009
+ "primary_commodity": "POT-RIR",
1010
+ "quantity_quintals": 25.7,
1011
+ "has_storage": true,
1012
+ "distance_to_market_km": 12.0,
1013
+ "nearest_market_id": "MKT-KRT",
1014
+ "notes": "Smallholder Irish potato farmer in Nyeri. 12.0 km to Karatina; has dry storage shed."
1015
+ },
1016
+ {
1017
+ "farmer_id": "FMR-K0079",
1018
+ "name": "Kariuki Nzioka",
1019
+ "location_name": "Nyeri",
1020
+ "latitude": -0.3688,
1021
+ "longitude": 37.0565,
1022
+ "primary_commodity": "BEN-YGN",
1023
+ "quantity_quintals": 24.9,
1024
+ "has_storage": true,
1025
+ "distance_to_market_km": 23.7,
1026
+ "nearest_market_id": "MKT-KRT",
1027
+ "notes": "Smallholder beans farmer in Nyeri. 23.7 km to Karatina; has dry storage shed."
1028
+ },
1029
+ {
1030
+ "farmer_id": "FMR-K0080",
1031
+ "name": "Fatuma Chepkoech",
1032
+ "location_name": "Nyeri",
1033
+ "latitude": -0.5787,
1034
+ "longitude": 37.0469,
1035
+ "primary_commodity": "BEN-YGN",
1036
+ "quantity_quintals": 16.9,
1037
+ "has_storage": false,
1038
+ "distance_to_market_km": 30.9,
1039
+ "nearest_market_id": "MKT-KRT",
1040
+ "notes": "Smallholder beans farmer in Nyeri. 30.9 km to Karatina; no storage; must sell at harvest."
1041
+ },
1042
+ {
1043
+ "farmer_id": "FMR-K0081",
1044
+ "name": "Barasa Mwangi",
1045
+ "location_name": "Nyeri",
1046
+ "latitude": -0.4783,
1047
+ "longitude": 37.0406,
1048
+ "primary_commodity": "MZE-DRY",
1049
+ "quantity_quintals": 30.2,
1050
+ "has_storage": false,
1051
+ "distance_to_market_km": 17.6,
1052
+ "nearest_market_id": "MKT-KRT",
1053
+ "notes": "Smallholder dry maize farmer in Nyeri. 17.6 km to Karatina; no storage; must sell at harvest."
1054
+ },
1055
+ {
1056
+ "farmer_id": "FMR-K0082",
1057
+ "name": "Kibet Kathambi",
1058
+ "location_name": "Nyeri",
1059
+ "latitude": -0.4988,
1060
+ "longitude": 36.9885,
1061
+ "primary_commodity": "BEN-YGN",
1062
+ "quantity_quintals": 12.8,
1063
+ "has_storage": true,
1064
+ "distance_to_market_km": 24.5,
1065
+ "nearest_market_id": "MKT-KRT",
1066
+ "notes": "Smallholder beans farmer in Nyeri. 24.5 km to Karatina; has dry storage shed."
1067
+ },
1068
+ {
1069
+ "farmer_id": "FMR-K0083",
1070
+ "name": "Mutembei Cherop",
1071
+ "location_name": "Nyeri",
1072
+ "latitude": -0.4913,
1073
+ "longitude": 37.1426,
1074
+ "primary_commodity": "BEN-YGN",
1075
+ "quantity_quintals": 39.9,
1076
+ "has_storage": true,
1077
+ "distance_to_market_km": 19.7,
1078
+ "nearest_market_id": "MKT-KRT",
1079
+ "notes": "Smallholder beans farmer in Nyeri. 19.7 km to Karatina; has dry storage shed."
1080
+ },
1081
+ {
1082
+ "farmer_id": "FMR-K0084",
1083
+ "name": "Korir Muthama",
1084
+ "location_name": "Nyeri",
1085
+ "latitude": -0.5413,
1086
+ "longitude": 37.0525,
1087
+ "primary_commodity": "POT-RIR",
1088
+ "quantity_quintals": 11.7,
1089
+ "has_storage": false,
1090
+ "distance_to_market_km": 5.1,
1091
+ "nearest_market_id": "MKT-KRT",
1092
+ "notes": "Smallholder Irish potato farmer in Nyeri. 5.1 km to Karatina; no storage; must sell at harvest."
1093
+ },
1094
+ {
1095
+ "farmer_id": "FMR-K0085",
1096
+ "name": "Otieno Wanjala",
1097
+ "location_name": "Nyeri",
1098
+ "latitude": -0.5684,
1099
+ "longitude": 37.1286,
1100
+ "primary_commodity": "POT-RIR",
1101
+ "quantity_quintals": 25.6,
1102
+ "has_storage": false,
1103
+ "distance_to_market_km": 26.8,
1104
+ "nearest_market_id": "MKT-KRT",
1105
+ "notes": "Smallholder Irish potato farmer in Nyeri. 26.8 km to Karatina; no storage; must sell at harvest."
1106
+ },
1107
+ {
1108
+ "farmer_id": "FMR-K0086",
1109
+ "name": "Maina Wanjala",
1110
+ "location_name": "Nyeri",
1111
+ "latitude": -0.5444,
1112
+ "longitude": 37.0916,
1113
+ "primary_commodity": "BEN-YGN",
1114
+ "quantity_quintals": 5.5,
1115
+ "has_storage": true,
1116
+ "distance_to_market_km": 19.2,
1117
+ "nearest_market_id": "MKT-KRT",
1118
+ "notes": "Smallholder beans farmer in Nyeri. 19.2 km to Karatina; has dry storage shed."
1119
+ },
1120
+ {
1121
+ "farmer_id": "FMR-K0087",
1122
+ "name": "Muthoni Mwende",
1123
+ "location_name": "Makueni",
1124
+ "latitude": -1.8908,
1125
+ "longitude": 37.5379,
1126
+ "primary_commodity": "BEN-YGN",
1127
+ "quantity_quintals": 8.6,
1128
+ "has_storage": true,
1129
+ "distance_to_market_km": 1.4,
1130
+ "nearest_market_id": "MKT-KTZ",
1131
+ "notes": "Smallholder beans farmer in Makueni. 1.4 km to Kathonzweni; has dry storage shed."
1132
+ },
1133
+ {
1134
+ "farmer_id": "FMR-K0088",
1135
+ "name": "Cherop Khamis",
1136
+ "location_name": "Makueni",
1137
+ "latitude": -1.9978,
1138
+ "longitude": 37.7896,
1139
+ "primary_commodity": "BEN-YGN",
1140
+ "quantity_quintals": 37.9,
1141
+ "has_storage": false,
1142
+ "distance_to_market_km": 17.2,
1143
+ "nearest_market_id": "MKT-KTZ",
1144
+ "notes": "Smallholder beans farmer in Makueni. 17.2 km to Kathonzweni; no storage; must sell at harvest."
1145
+ },
1146
+ {
1147
+ "farmer_id": "FMR-K0089",
1148
+ "name": "Kipchumba Kathambi",
1149
+ "location_name": "Makueni",
1150
+ "latitude": -2.0778,
1151
+ "longitude": 37.6888,
1152
+ "primary_commodity": "MZE-DRY",
1153
+ "quantity_quintals": 10.3,
1154
+ "has_storage": false,
1155
+ "distance_to_market_km": 28.6,
1156
+ "nearest_market_id": "MKT-KTZ",
1157
+ "notes": "Smallholder dry maize farmer in Makueni. 28.6 km to Kathonzweni; no storage; must sell at harvest."
1158
+ },
1159
+ {
1160
+ "farmer_id": "FMR-K0090",
1161
+ "name": "Mwangi Mueni",
1162
+ "location_name": "Makueni",
1163
+ "latitude": -2.0645,
1164
+ "longitude": 37.7293,
1165
+ "primary_commodity": "GRG-KEN",
1166
+ "quantity_quintals": 25.9,
1167
+ "has_storage": false,
1168
+ "distance_to_market_km": 22.0,
1169
+ "nearest_market_id": "MKT-KTZ",
1170
+ "notes": "Smallholder green grams farmer in Makueni. 22.0 km to Kathonzweni; no storage; must sell at harvest."
1171
+ },
1172
+ {
1173
+ "farmer_id": "FMR-K0091",
1174
+ "name": "Nzioka Abdalla",
1175
+ "location_name": "Makueni",
1176
+ "latitude": -1.8274,
1177
+ "longitude": 37.5892,
1178
+ "primary_commodity": "GRG-KEN",
1179
+ "quantity_quintals": 39.7,
1180
+ "has_storage": false,
1181
+ "distance_to_market_km": 11.4,
1182
+ "nearest_market_id": "MKT-KTZ",
1183
+ "notes": "Smallholder green grams farmer in Makueni. 11.4 km to Kathonzweni; no storage; must sell at harvest."
1184
+ },
1185
+ {
1186
+ "farmer_id": "FMR-K0092",
1187
+ "name": "Kwamboka Ochieng",
1188
+ "location_name": "Makueni",
1189
+ "latitude": -1.8157,
1190
+ "longitude": 37.5623,
1191
+ "primary_commodity": "MZE-DRY",
1192
+ "quantity_quintals": 14.3,
1193
+ "has_storage": true,
1194
+ "distance_to_market_km": 3.8,
1195
+ "nearest_market_id": "MKT-KTZ",
1196
+ "notes": "Smallholder dry maize farmer in Makueni. 3.8 km to Kathonzweni; has dry storage shed."
1197
+ },
1198
+ {
1199
+ "farmer_id": "FMR-K0093",
1200
+ "name": "Rehema Mbugua",
1201
+ "location_name": "Makueni",
1202
+ "latitude": -2.053,
1203
+ "longitude": 37.6226,
1204
+ "primary_commodity": "GRG-KEN",
1205
+ "quantity_quintals": 25.8,
1206
+ "has_storage": true,
1207
+ "distance_to_market_km": 10.9,
1208
+ "nearest_market_id": "MKT-KTZ",
1209
+ "notes": "Smallholder green grams farmer in Makueni. 10.9 km to Kathonzweni; has dry storage shed."
1210
+ },
1211
+ {
1212
+ "farmer_id": "FMR-K0094",
1213
+ "name": "Simiyu Mwenda",
1214
+ "location_name": "Tharaka Nithi",
1215
+ "latitude": -0.2936,
1216
+ "longitude": 37.68,
1217
+ "primary_commodity": "BEN-YGN",
1218
+ "quantity_quintals": 32.6,
1219
+ "has_storage": false,
1220
+ "distance_to_market_km": 22.3,
1221
+ "nearest_market_id": "MKT-KAN",
1222
+ "notes": "Smallholder beans farmer in Tharaka Nithi. 22.3 km to Kaanwa; no storage; must sell at harvest."
1223
+ },
1224
+ {
1225
+ "farmer_id": "FMR-K0095",
1226
+ "name": "Owino Ali",
1227
+ "location_name": "Tharaka Nithi",
1228
+ "latitude": -0.2806,
1229
+ "longitude": 37.6774,
1230
+ "primary_commodity": "MZE-DRY",
1231
+ "quantity_quintals": 36.8,
1232
+ "has_storage": true,
1233
+ "distance_to_market_km": 29.1,
1234
+ "nearest_market_id": "MKT-KAN",
1235
+ "notes": "Smallholder dry maize farmer in Tharaka Nithi. 29.1 km to Kaanwa; has dry storage shed."
1236
+ },
1237
+ {
1238
+ "farmer_id": "FMR-K0096",
1239
+ "name": "Awino Kiprop",
1240
+ "location_name": "Tharaka Nithi",
1241
+ "latitude": -0.1365,
1242
+ "longitude": 37.8573,
1243
+ "primary_commodity": "BEN-YGN",
1244
+ "quantity_quintals": 20.4,
1245
+ "has_storage": true,
1246
+ "distance_to_market_km": 16.9,
1247
+ "nearest_market_id": "MKT-KAN",
1248
+ "notes": "Smallholder beans farmer in Tharaka Nithi. 16.9 km to Kaanwa; has dry storage shed."
1249
+ },
1250
+ {
1251
+ "farmer_id": "FMR-K0097",
1252
+ "name": "Kipchumba Muthama",
1253
+ "location_name": "Tharaka Nithi",
1254
+ "latitude": -0.1649,
1255
+ "longitude": 37.7581,
1256
+ "primary_commodity": "MZE-DRY",
1257
+ "quantity_quintals": 25.9,
1258
+ "has_storage": false,
1259
+ "distance_to_market_km": 31.2,
1260
+ "nearest_market_id": "MKT-KAN",
1261
+ "notes": "Smallholder dry maize farmer in Tharaka Nithi. 31.2 km to Kaanwa; no storage; must sell at harvest."
1262
+ },
1263
+ {
1264
+ "farmer_id": "FMR-K0098",
1265
+ "name": "Karimi Ndinda",
1266
+ "location_name": "Tharaka Nithi",
1267
+ "latitude": -0.3072,
1268
+ "longitude": 37.6787,
1269
+ "primary_commodity": "GRG-KEN",
1270
+ "quantity_quintals": 34.3,
1271
+ "has_storage": false,
1272
+ "distance_to_market_km": 20.6,
1273
+ "nearest_market_id": "MKT-KAN",
1274
+ "notes": "Smallholder green grams farmer in Tharaka Nithi. 20.6 km to Kaanwa; no storage; must sell at harvest."
1275
+ },
1276
+ {
1277
+ "farmer_id": "FMR-K0099",
1278
+ "name": "Wafula Maina",
1279
+ "location_name": "Tharaka Nithi",
1280
+ "latitude": -0.0861,
1281
+ "longitude": 37.6626,
1282
+ "primary_commodity": "GRG-KEN",
1283
+ "quantity_quintals": 27.8,
1284
+ "has_storage": true,
1285
+ "distance_to_market_km": 32.1,
1286
+ "nearest_market_id": "MKT-KAN",
1287
+ "notes": "Smallholder green grams farmer in Tharaka Nithi. 32.1 km to Kaanwa; has dry storage shed."
1288
+ },
1289
+ {
1290
+ "farmer_id": "FMR-K0100",
1291
+ "name": "Mutembei Mohamed",
1292
+ "location_name": "Tharaka Nithi",
1293
+ "latitude": -0.1246,
1294
+ "longitude": 37.7575,
1295
+ "primary_commodity": "MZE-DRY",
1296
+ "quantity_quintals": 21.5,
1297
+ "has_storage": true,
1298
+ "distance_to_market_km": 14.9,
1299
+ "nearest_market_id": "MKT-KAN",
1300
+ "notes": "Smallholder dry maize farmer in Tharaka Nithi. 14.9 km to Kaanwa; has dry storage shed."
1301
+ }
1302
+ ]
markets_kenya.json ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "mandi_id": "MKT-KIB",
4
+ "name": "Kibuye",
5
+ "district": "Kisumu",
6
+ "state": "Kisumu",
7
+ "latitude": -0.0952,
8
+ "longitude": 34.7523,
9
+ "market_type": "wholesale",
10
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "GRG-KEN"],
11
+ "avg_daily_arrivals_tonnes": 340.0,
12
+ "enam_integrated": false,
13
+ "reporting_quality": "good"
14
+ },
15
+ {
16
+ "mandi_id": "MKT-NWK",
17
+ "name": "Nairobi Wakulima",
18
+ "district": "Nairobi",
19
+ "state": "Nairobi",
20
+ "latitude": -1.2921,
21
+ "longitude": 36.8270,
22
+ "market_type": "terminal",
23
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "POT-RIR", "GRG-KEN"],
24
+ "avg_daily_arrivals_tonnes": 620.0,
25
+ "enam_integrated": false,
26
+ "reporting_quality": "good"
27
+ },
28
+ {
29
+ "mandi_id": "MKT-KON",
30
+ "name": "Kongowea",
31
+ "district": "Mombasa",
32
+ "state": "Mombasa",
33
+ "latitude": -4.0277,
34
+ "longitude": 39.6876,
35
+ "market_type": "terminal",
36
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "POT-RIR", "GRG-KEN"],
37
+ "avg_daily_arrivals_tonnes": 480.0,
38
+ "enam_integrated": false,
39
+ "reporting_quality": "good"
40
+ },
41
+ {
42
+ "mandi_id": "MKT-ELD",
43
+ "name": "Eldoret Main",
44
+ "district": "Uasin Gishu",
45
+ "state": "Uasin Gishu",
46
+ "latitude": 0.5143,
47
+ "longitude": 35.2698,
48
+ "market_type": "wholesale",
49
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "POT-RIR"],
50
+ "avg_daily_arrivals_tonnes": 410.0,
51
+ "enam_integrated": false,
52
+ "reporting_quality": "good"
53
+ },
54
+ {
55
+ "mandi_id": "MKT-NAK",
56
+ "name": "Nakuru Wakulima",
57
+ "district": "Nakuru",
58
+ "state": "Nakuru",
59
+ "latitude": -0.3031,
60
+ "longitude": 36.0800,
61
+ "market_type": "wholesale",
62
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "POT-RIR"],
63
+ "avg_daily_arrivals_tonnes": 295.0,
64
+ "enam_integrated": false,
65
+ "reporting_quality": "good"
66
+ },
67
+ {
68
+ "mandi_id": "MKT-KTG",
69
+ "name": "Kitengela",
70
+ "district": "Kajiado",
71
+ "state": "Kajiado",
72
+ "latitude": -1.4747,
73
+ "longitude": 36.9587,
74
+ "market_type": "regulated",
75
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "GRG-KEN"],
76
+ "avg_daily_arrivals_tonnes": 95.0,
77
+ "enam_integrated": false,
78
+ "reporting_quality": "poor"
79
+ },
80
+ {
81
+ "mandi_id": "MKT-KGM",
82
+ "name": "Kangemi Market",
83
+ "district": "Nairobi",
84
+ "state": "Nairobi",
85
+ "latitude": -1.2680,
86
+ "longitude": 36.7462,
87
+ "market_type": "wholesale",
88
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "POT-RIR"],
89
+ "avg_daily_arrivals_tonnes": 175.0,
90
+ "enam_integrated": false,
91
+ "reporting_quality": "moderate"
92
+ },
93
+ {
94
+ "mandi_id": "MKT-KRT",
95
+ "name": "Karatina",
96
+ "district": "Nyeri",
97
+ "state": "Nyeri",
98
+ "latitude": -0.4833,
99
+ "longitude": 37.1333,
100
+ "market_type": "wholesale",
101
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "POT-RIR"],
102
+ "avg_daily_arrivals_tonnes": 230.0,
103
+ "enam_integrated": false,
104
+ "reporting_quality": "good"
105
+ },
106
+ {
107
+ "mandi_id": "MKT-KTZ",
108
+ "name": "Kathonzweni",
109
+ "district": "Makueni",
110
+ "state": "Makueni",
111
+ "latitude": -1.9500,
112
+ "longitude": 37.6500,
113
+ "market_type": "regulated",
114
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "GRG-KEN"],
115
+ "avg_daily_arrivals_tonnes": 85.0,
116
+ "enam_integrated": false,
117
+ "reporting_quality": "moderate"
118
+ },
119
+ {
120
+ "mandi_id": "MKT-KAN",
121
+ "name": "Kaanwa",
122
+ "district": "Tharaka Nithi",
123
+ "state": "Tharaka Nithi",
124
+ "latitude": -0.1650,
125
+ "longitude": 37.7600,
126
+ "market_type": "regulated",
127
+ "commodities_traded": ["MZE-DRY", "BEN-YGN", "GRG-KEN"],
128
+ "avg_daily_arrivals_tonnes": 70.0,
129
+ "enam_integrated": false,
130
+ "reporting_quality": "poor"
131
+ }
132
+ ]
src/ingestion/kamis.py ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KAMIS (Kenya Agricultural Market Information System) price client.
3
+
4
+ Primary data source for Kenya wholesale/retail prices, run by the Kenya
5
+ Ministry of Agriculture, Livestock and Fisheries. It publishes daily
6
+ market reports across 47 counties with wholesale and retail quotes per
7
+ commodity, variety, and market via the public search portal at
8
+ `https://kamis.kilimo.go.ke/site/market_search`.
9
+
10
+ The portal's HTTPS cert is expired/self-signed as of 2026-04 so we
11
+ disable verification (the data is public).
12
+
13
+ Shape mirrors `src.ingestion.agmarknet.fetch_mandi_prices` so the
14
+ `pipeline.MarketIntelligencePipeline._step_ingest` helpers can plug
15
+ this in as a sibling `PriceSource` once Phase 1.1 wires the region
16
+ dispatch.
17
+
18
+ Implementation ported from
19
+ `lastmile-bench/lastmile_bench/benchmarks/market_intelligence/sources/kamis.py`
20
+ (the benchmark reference) — trimmed to drop pydantic + tenacity so we
21
+ don't take new dependencies on the production app.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import csv
28
+ import logging
29
+ import os
30
+ import re
31
+ import time
32
+ from datetime import date, datetime, timedelta
33
+ from pathlib import Path
34
+ from typing import Any, Iterable, Sequence
35
+ from urllib.parse import urlencode
36
+
37
+ import httpx
38
+ from bs4 import BeautifulSoup
39
+
40
+ from src.ingestion.agmarknet import PriceRecord
41
+
42
+ log = logging.getLogger(__name__)
43
+
44
+ # --- KAMIS portal configuration -----------------------------------------
45
+
46
+ PORTAL_BASE_URL = "https://kamis.kilimo.go.ke/site/market_search"
47
+
48
+ KAMIS_ATTRIBUTION = (
49
+ "Kenya Ministry of Agriculture, Livestock and Fisheries, "
50
+ "Kenya Agricultural Market Information System (KAMIS). "
51
+ "Daily wholesale and retail price reports by market and county, "
52
+ "scraped from https://kamis.kilimo.go.ke under the Kenya Access "
53
+ "to Information Act 2016 transparency provisions."
54
+ )
55
+
56
+ # KAMIS product catalog. Integer IDs come from the portal's
57
+ # `<option value="N">Name</option>` tags on the search page.
58
+ PRODUCT_IDS: dict[str, int] = {
59
+ "Dry maize": 1,
60
+ "Red sorghum": 2,
61
+ "Wheat": 3,
62
+ "Rice": 4,
63
+ "Green grams": 10,
64
+ "Ground nuts": 12,
65
+ "Beans (yellow-green)": 30,
66
+ "Finger millet": 54,
67
+ "White sorghum": 56,
68
+ "Red irish potato": 57,
69
+ "Sweet potatoes": 59,
70
+ }
71
+
72
+ # Aliases so callers can pass a commodity dict whose `name` or `kamis_name`
73
+ # lands on a canonical key in PRODUCT_IDS. Keep this small; Phase 1.1 will
74
+ # move the canonical mapping into `commodities.json::kamis_name`.
75
+ _COMMODITY_ALIASES: dict[str, str] = {
76
+ "maize": "Dry maize",
77
+ "dry maize": "Dry maize",
78
+ "beans": "Beans (yellow-green)",
79
+ "green grams": "Green grams",
80
+ "red irish potato": "Red irish potato",
81
+ "irish potato": "Red irish potato",
82
+ "potato": "Red irish potato",
83
+ }
84
+
85
+ _POLITE_DELAY_SECONDS = 3.0
86
+ _MAX_RETRIES = 5
87
+ _TIMEOUT_SECONDS = 45.0
88
+ _USER_AGENT = (
89
+ "Mozilla/5.0 (compatible; market-intelligence/0.1; "
90
+ "+https://crop-pricing.jeff-levine.com) research scraper"
91
+ )
92
+
93
+ # Module-level timestamp backing the polite-delay throttle. KAMIS has no
94
+ # published rate limit; 3s/request matches the benchmark reference and
95
+ # avoids tripping whatever the portal's operational limits are.
96
+ _last_request_at = 0.0
97
+
98
+
99
+ # --- Public entry point --------------------------------------------------
100
+
101
+
102
+ async def fetch_kamis_prices(
103
+ markets: Sequence[Any] | None = None,
104
+ commodities: Sequence[dict] | None = None,
105
+ days_back: int = 30,
106
+ ) -> dict[str, list[PriceRecord]]:
107
+ """Fetch daily KAMIS prices and return the Agmarknet PriceRecord shape.
108
+
109
+ Parameters
110
+ ----------
111
+ markets
112
+ Iterable of market/mandi objects. Each must expose `mandi_id`,
113
+ `name`, and `district` attributes (the current `config.Mandi`
114
+ dataclass satisfies this; a Phase 1.1 Kenya `Market` dataclass
115
+ will also satisfy this if it keeps the same field names, or a
116
+ plain dict with those keys will work via the adaptor below).
117
+ If None, raises — KAMIS has no implicit region default.
118
+ commodities
119
+ Iterable of commodity dicts (as loaded from commodities.json).
120
+ Each must have an `id` key and at least one of: `kamis_name`,
121
+ `name`, or an entry in `_COMMODITY_ALIASES`.
122
+ days_back
123
+ How many days of history to fetch per (market, commodity).
124
+
125
+ Returns
126
+ -------
127
+ dict[str, list[PriceRecord]]
128
+ Price records keyed by `mandi_id`, each record with `source="kamis"`.
129
+ Currency is KES, unit is "kg" (KAMIS publishes KES per kilogram);
130
+ downstream reconciliation / forecasting treats `modal_price_rs` as
131
+ a numeric local-currency-per-standard-unit quote regardless of
132
+ region, matching how the portable base `PriceSource` protocol
133
+ already documents `currency` as caller-supplied.
134
+
135
+ Demo mode
136
+ ---------
137
+ Setting ``MARKET_INTEL_DEMO_MODE`` to a truthy value routes reads
138
+ through a committed CSV snapshot at
139
+ ``data/demo_snapshots/kamis/kamis_demo_snapshot.csv`` so tests and
140
+ the weekly pipeline can run deterministically without hitting the
141
+ live portal. Missing snapshot → empty result + a warning (the
142
+ pipeline is designed to tolerate empty ingestion from any single
143
+ source).
144
+ """
145
+ if markets is None or commodities is None:
146
+ raise ValueError(
147
+ "fetch_kamis_prices requires explicit markets + commodities; "
148
+ "KAMIS has no implicit region default."
149
+ )
150
+
151
+ market_list = list(markets)
152
+ commodity_list = list(commodities)
153
+
154
+ demo_mode = os.environ.get("MARKET_INTEL_DEMO_MODE", "").lower() in (
155
+ "1", "true", "yes"
156
+ )
157
+ if demo_mode:
158
+ log.info(
159
+ "KAMIS: DEMO mode — reading snapshot for %d markets x %d commodities",
160
+ len(market_list), len(commodity_list),
161
+ )
162
+ return _load_demo_snapshot(market_list, commodity_list, days_back)
163
+
164
+ log.info(
165
+ "KAMIS: LIVE mode — fetching from %s for %d markets x %d commodities",
166
+ PORTAL_BASE_URL, len(market_list), len(commodity_list),
167
+ )
168
+ return await _fetch_live(market_list, commodity_list, days_back)
169
+
170
+
171
+ # --- Live scrape path ----------------------------------------------------
172
+
173
+
174
+ async def _fetch_live(
175
+ markets: list[Any],
176
+ commodities: list[dict],
177
+ days_back: int,
178
+ ) -> dict[str, list[PriceRecord]]:
179
+ """Hit the live portal with a 3s polite delay + retry-with-backoff.
180
+
181
+ Runs the blocking httpx call inside `asyncio.to_thread` so this
182
+ coroutine composes with the pipeline's asyncio.gather without us
183
+ having to rewrite the parser async-native.
184
+ """
185
+ end = date.today()
186
+ start = end - timedelta(days=days_back)
187
+ results: dict[str, list[PriceRecord]] = {_market_id(m): [] for m in markets}
188
+
189
+ # Suppress urllib3 InsecureRequestWarning once up front (KAMIS cert is expired).
190
+ import urllib3
191
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
192
+
193
+ for commodity in commodities:
194
+ product_id = _resolve_product_id(commodity)
195
+ if product_id is None:
196
+ log.warning(
197
+ "KAMIS: no PRODUCT_IDS mapping for commodity %s — skipping",
198
+ commodity.get("id") or commodity.get("name"),
199
+ )
200
+ continue
201
+
202
+ for market in markets:
203
+ county = _market_county(market)
204
+ url = build_search_url(
205
+ product_id=product_id,
206
+ county=county,
207
+ start=start,
208
+ end=end,
209
+ )
210
+ try:
211
+ html = await asyncio.to_thread(_fetch_html_with_retry, url)
212
+ except Exception as exc:
213
+ log.warning(
214
+ "KAMIS: fetch failed for %s / %s (%s): %s",
215
+ _market_id(market), commodity.get("id"), url, exc,
216
+ )
217
+ continue
218
+
219
+ raw_records = parse_search_result_html(html)
220
+ filtered = _filter_by_market(raw_records, market)
221
+ for rec in filtered:
222
+ results[_market_id(market)].append(
223
+ _to_price_record(rec, market=market, commodity=commodity)
224
+ )
225
+
226
+ total = sum(len(v) for v in results.values())
227
+ log.info("KAMIS: fetched %d price records across %d markets", total, len(markets))
228
+ return results
229
+
230
+
231
+ def _fetch_html_with_retry(url: str) -> str:
232
+ """Synchronous GET with 3s polite delay + exponential backoff.
233
+
234
+ KAMIS's cert is expired/self-signed → `verify=False`. We don't fall
235
+ back to HTTP because the portal 301-redirects HTTP→HTTPS.
236
+ """
237
+ last_exc: Exception | None = None
238
+ for attempt in range(_MAX_RETRIES):
239
+ _polite_delay()
240
+ try:
241
+ with httpx.Client(
242
+ follow_redirects=True,
243
+ timeout=_TIMEOUT_SECONDS,
244
+ headers={"User-Agent": _USER_AGENT, "Accept": "text/html"},
245
+ verify=False,
246
+ ) as client:
247
+ resp = client.get(url)
248
+ resp.raise_for_status()
249
+ return resp.text
250
+ except (httpx.HTTPError, httpx.TimeoutException) as exc:
251
+ last_exc = exc
252
+ backoff = min(30.0, 2.0 * (2 ** attempt))
253
+ log.debug(
254
+ "KAMIS: fetch attempt %d/%d failed (%s); retrying in %.1fs",
255
+ attempt + 1, _MAX_RETRIES, exc, backoff,
256
+ )
257
+ time.sleep(backoff)
258
+ assert last_exc is not None
259
+ raise last_exc
260
+
261
+
262
+ def _polite_delay() -> None:
263
+ """Block until `_POLITE_DELAY_SECONDS` have elapsed since the last call."""
264
+ global _last_request_at
265
+ now = time.monotonic()
266
+ elapsed = now - _last_request_at
267
+ if elapsed < _POLITE_DELAY_SECONDS:
268
+ time.sleep(_POLITE_DELAY_SECONDS - elapsed)
269
+ _last_request_at = time.monotonic()
270
+
271
+
272
+ # --- URL + HTML parsing (ported from the benchmark reference) -----------
273
+
274
+
275
+ def build_search_url(
276
+ *,
277
+ product_id: int,
278
+ county: str | None = None,
279
+ start: date,
280
+ end: date,
281
+ per_page: int = 2000,
282
+ ) -> str:
283
+ """Build a KAMIS market_search URL with ISO-format dates.
284
+
285
+ The localized date form in the UI fails silently. `county` is
286
+ optional; if None, the query spans all counties.
287
+ """
288
+ params: list[tuple[str, str]] = [
289
+ ("product[]", str(product_id)),
290
+ ("start", start.isoformat()),
291
+ ("end", end.isoformat()),
292
+ ("per_page", str(per_page)),
293
+ ]
294
+ if county is not None:
295
+ params.insert(0, ("county[]", county))
296
+ return f"{PORTAL_BASE_URL}?{urlencode(params)}"
297
+
298
+
299
+ _DATE_FORMATS = ("%Y-%m-%d", "%d-%b-%Y", "%d/%m/%Y")
300
+ _PRICE_RE = re.compile(r"([0-9][0-9,]*(?:\.[0-9]+)?)")
301
+
302
+
303
+ def _parse_price(raw: str) -> float | None:
304
+ cleaned = (raw or "").strip().replace(",", "")
305
+ if not cleaned or cleaned in {"-", "--"}:
306
+ return None
307
+ m = _PRICE_RE.search(cleaned)
308
+ if not m:
309
+ return None
310
+ try:
311
+ return float(m.group(1))
312
+ except ValueError:
313
+ return None
314
+
315
+
316
+ def _parse_date(raw: str) -> date | None:
317
+ cleaned = re.sub(r"\s+", " ", raw or "").strip()
318
+ for fmt in _DATE_FORMATS:
319
+ try:
320
+ return datetime.strptime(cleaned, fmt).date()
321
+ except ValueError:
322
+ continue
323
+ return None
324
+
325
+
326
+ def parse_search_result_html(html: str) -> list[dict[str, Any]]:
327
+ """Parse a KAMIS /site/market_search response into raw row dicts.
328
+
329
+ Returns dicts (not a typed record) so the caller can map into the
330
+ app's existing `PriceRecord` dataclass. Each dict has keys:
331
+ market, product, variety, wholesale, retail, volume, county,
332
+ price_date.
333
+ Rows missing both prices or with an unparseable date are dropped.
334
+ """
335
+ soup = BeautifulSoup(html, "html.parser")
336
+ tbody = soup.find("tbody")
337
+ if tbody is None:
338
+ return []
339
+ out: list[dict[str, Any]] = []
340
+ for tr in tbody.find_all("tr", recursive=False):
341
+ cells = [td.get_text(strip=True) for td in tr.find_all("td")]
342
+ if len(cells) != 10:
343
+ continue
344
+ wholesale = _parse_price(cells[5])
345
+ retail = _parse_price(cells[6])
346
+ price_date = _parse_date(cells[9])
347
+ if price_date is None:
348
+ continue
349
+ if wholesale is None and retail is None:
350
+ continue
351
+ wholesale = wholesale if wholesale is not None else retail
352
+ retail = retail if retail is not None else wholesale
353
+ out.append({
354
+ "market": cells[0],
355
+ "product": cells[1],
356
+ "variety": cells[2],
357
+ "wholesale": wholesale,
358
+ "retail": retail,
359
+ "volume": cells[7],
360
+ "county": cells[8],
361
+ "price_date": price_date,
362
+ })
363
+ return out
364
+
365
+
366
+ # --- Demo-mode snapshot reader ------------------------------------------
367
+
368
+
369
+ def _demo_snapshot_path() -> Path:
370
+ """Resolve the committed KAMIS snapshot relative to the project root."""
371
+ # src/ingestion/kamis.py → project root is parents[2]
372
+ return (
373
+ Path(__file__).resolve().parents[2]
374
+ / "data" / "demo_snapshots" / "kamis" / "kamis_demo_snapshot.csv"
375
+ )
376
+
377
+
378
+ def _load_demo_snapshot(
379
+ markets: list[Any],
380
+ commodities: list[dict],
381
+ days_back: int,
382
+ ) -> dict[str, list[PriceRecord]]:
383
+ """Read the committed CSV snapshot and filter to requested markets × commodities.
384
+
385
+ The snapshot has the same 10-column shape as
386
+ `records_to_csv_rows` in the benchmark — this lets us reuse
387
+ lastmile-bench raw_cache exports unchanged.
388
+ """
389
+ path = _demo_snapshot_path()
390
+ results: dict[str, list[PriceRecord]] = {_market_id(m): [] for m in markets}
391
+ if not path.exists():
392
+ log.warning("KAMIS demo: snapshot missing at %s — returning empty", path)
393
+ return results
394
+
395
+ commodity_lookup = _build_commodity_lookup(commodities)
396
+ if not commodity_lookup:
397
+ log.warning("KAMIS demo: no commodity name mappings — returning empty")
398
+ return results
399
+
400
+ # Build a market lookup keyed by normalized market name so we can
401
+ # match against the "Market" CSV column. Duck-typed — works for
402
+ # `config.Mandi` and for any Phase 1.1 Kenya market object that
403
+ # exposes `name`.
404
+ market_by_name: dict[str, Any] = {
405
+ _norm(getattr(m, "name", "")): m for m in markets
406
+ }
407
+
408
+ with open(path, newline="", encoding="utf-8") as fh:
409
+ reader = csv.DictReader(fh)
410
+ for row in reader:
411
+ market_obj = market_by_name.get(_norm(row.get("Market", "")))
412
+ if market_obj is None:
413
+ continue
414
+ commodity = commodity_lookup.get(_norm(row.get("Commodity", "")))
415
+ if commodity is None:
416
+ continue
417
+ price_date = _parse_date(row.get("Price Date", ""))
418
+ if price_date is None:
419
+ continue
420
+ modal = _safe_float(row.get("Modal Price"))
421
+ if modal is None or modal <= 0:
422
+ continue
423
+ min_price = _safe_float(row.get("Min Price")) or modal
424
+ max_price = _safe_float(row.get("Max Price")) or modal
425
+ arrivals = _safe_float(row.get("Arrivals (Tonnes)")) or 0.0
426
+ results[_market_id(market_obj)].append(PriceRecord(
427
+ mandi_id=_market_id(market_obj),
428
+ commodity_id=commodity["id"],
429
+ date=price_date.strftime("%Y-%m-%d"),
430
+ min_price_rs=min_price,
431
+ max_price_rs=max_price,
432
+ modal_price_rs=modal,
433
+ arrivals_tonnes=arrivals,
434
+ source="kamis",
435
+ freshness_hours=24.0,
436
+ quality_flag="good",
437
+ ))
438
+
439
+ total = sum(len(v) for v in results.values())
440
+ log.info("KAMIS demo: loaded %d records across %d markets from %s",
441
+ total, len(markets), path.name)
442
+ return results
443
+
444
+
445
+ # --- Helpers -------------------------------------------------------------
446
+
447
+
448
+ def _resolve_product_id(commodity: dict) -> int | None:
449
+ """Map a commodity dict to a KAMIS product_id.
450
+
451
+ Tries `commodity["kamis_name"]`, `commodity["name"]`,
452
+ `commodity["id"]`, then the alias table. Returns None if nothing
453
+ matches — the caller logs and skips.
454
+ """
455
+ for key in ("kamis_name", "name", "id"):
456
+ raw = commodity.get(key)
457
+ if not raw:
458
+ continue
459
+ if raw in PRODUCT_IDS:
460
+ return PRODUCT_IDS[raw]
461
+ canonical = _COMMODITY_ALIASES.get(_norm(raw))
462
+ if canonical and canonical in PRODUCT_IDS:
463
+ return PRODUCT_IDS[canonical]
464
+ return None
465
+
466
+
467
+ def _build_commodity_lookup(commodities: list[dict]) -> dict[str, dict]:
468
+ """Map normalized commodity names back to the commodity dict.
469
+
470
+ Used by the demo reader to assign `commodity_id` to each CSV row.
471
+ """
472
+ out: dict[str, dict] = {}
473
+ for c in commodities:
474
+ for key in ("kamis_name", "name", "id"):
475
+ val = c.get(key)
476
+ if val:
477
+ out[_norm(val)] = c
478
+ # also register the aliased canonical name
479
+ for key in ("kamis_name", "name", "id"):
480
+ val = c.get(key)
481
+ canonical = _COMMODITY_ALIASES.get(_norm(val or ""))
482
+ if canonical:
483
+ out[_norm(canonical)] = c
484
+ return out
485
+
486
+
487
+ def _filter_by_market(
488
+ rows: list[dict[str, Any]], market: Any
489
+ ) -> list[dict[str, Any]]:
490
+ """Keep only the rows whose "market" cell matches the passed market.
491
+
492
+ KAMIS county queries return all markets in the county — we filter
493
+ client-side to honor the caller's `markets=` argument.
494
+ """
495
+ target = _norm(getattr(market, "name", ""))
496
+ if not target:
497
+ return rows
498
+ return [r for r in rows if _norm(r.get("market", "")) == target]
499
+
500
+
501
+ def _to_price_record(
502
+ raw: dict[str, Any], *, market: Any, commodity: dict
503
+ ) -> PriceRecord:
504
+ """Convert a parsed KAMIS row into the app's Agmarknet-shaped PriceRecord.
505
+
506
+ We emit the **retail** price as `modal_price_rs` (KAMIS doesn't
507
+ publish a modal; retail is the consumer-facing quote that a farmer
508
+ would compare a selling offer against), wholesale as `min_price_rs`,
509
+ and retail as `max_price_rs`. `arrivals_tonnes` stays 0 because
510
+ KAMIS's "volume" field is an unconfirmed unit.
511
+ """
512
+ wholesale = float(raw["wholesale"])
513
+ retail = float(raw["retail"])
514
+ return PriceRecord(
515
+ mandi_id=_market_id(market),
516
+ commodity_id=commodity["id"],
517
+ date=raw["price_date"].strftime("%Y-%m-%d"),
518
+ min_price_rs=wholesale,
519
+ max_price_rs=retail,
520
+ modal_price_rs=retail,
521
+ arrivals_tonnes=0.0,
522
+ source="kamis",
523
+ freshness_hours=24.0,
524
+ quality_flag="good",
525
+ )
526
+
527
+
528
+ def _market_id(market: Any) -> str:
529
+ """Return the market's ID, tolerating both `mandi_id` and `market_id`."""
530
+ for attr in ("mandi_id", "market_id", "id"):
531
+ val = getattr(market, attr, None)
532
+ if val:
533
+ return str(val)
534
+ if isinstance(market, dict):
535
+ for k in ("mandi_id", "market_id", "id"):
536
+ if market.get(k):
537
+ return str(market[k])
538
+ raise ValueError(f"Market has no mandi_id / market_id / id: {market!r}")
539
+
540
+
541
+ def _market_county(market: Any) -> str | None:
542
+ """Best-effort extraction of a KAMIS-compatible county string.
543
+
544
+ Tries `county`, then `district`, then `state`. Returns None if
545
+ nothing is set so the URL builder queries all counties.
546
+ """
547
+ for attr in ("county", "district", "state"):
548
+ val = getattr(market, attr, None)
549
+ if val:
550
+ return str(val)
551
+ if isinstance(market, dict):
552
+ for k in ("county", "district", "state"):
553
+ if market.get(k):
554
+ return str(market[k])
555
+ return None
556
+
557
+
558
+ def _norm(s: str) -> str:
559
+ return re.sub(r"\s+", " ", (s or "").strip().lower())
560
+
561
+
562
+ def _safe_float(v: Any) -> float | None:
563
+ if v is None or v == "":
564
+ return None
565
+ try:
566
+ return float(v)
567
+ except (TypeError, ValueError):
568
+ return None
569
+
570
+
571
+ __all__ = [
572
+ "PORTAL_BASE_URL",
573
+ "KAMIS_ATTRIBUTION",
574
+ "PRODUCT_IDS",
575
+ "fetch_kamis_prices",
576
+ "build_search_url",
577
+ "parse_search_result_html",
578
+ ]
src/pipeline.py CHANGED
@@ -22,6 +22,7 @@ from config import (
22
  MANDIS,
23
  MANDI_MAP,
24
  PIPELINE_STEPS,
 
25
  SAMPLE_FARMERS,
26
  FEATURED_FARMERS,
27
  SEASONAL_INDICES,
@@ -29,6 +30,7 @@ from config import (
29
  )
30
  from src.ingestion.agmarknet import fetch_mandi_prices, PriceRecord
31
  from src.ingestion.enam_scraper import fetch_enam_prices
 
32
  from src.ingestion.nasa_power import fetch_all_mandis_nasa_power, DailyReading
33
  from src.extraction.agent import ExtractionAgent, RuleBasedExtractor
34
  from src.reconciliation.agent import ReconciliationAgent, RuleBasedReconciler
@@ -226,6 +228,15 @@ class MarketIntelligencePipeline:
226
 
227
  try:
228
  async def _ingest_agmarknet():
 
 
 
 
 
 
 
 
 
229
  try:
230
  self._agmarknet_prices = await fetch_mandi_prices(
231
  MANDIS, COMMODITIES, days_back=self.days_back,
@@ -234,6 +245,9 @@ class MarketIntelligencePipeline:
234
  errors.append(f"Agmarknet fetch failed: {e}")
235
 
236
  async def _ingest_enam():
 
 
 
237
  try:
238
  self._enam_prices = await fetch_enam_prices(
239
  MANDIS, COMMODITIES, days_back=min(14, self.days_back),
 
22
  MANDIS,
23
  MANDI_MAP,
24
  PIPELINE_STEPS,
25
+ REGION,
26
  SAMPLE_FARMERS,
27
  FEATURED_FARMERS,
28
  SEASONAL_INDICES,
 
30
  )
31
  from src.ingestion.agmarknet import fetch_mandi_prices, PriceRecord
32
  from src.ingestion.enam_scraper import fetch_enam_prices
33
+ from src.ingestion.kamis import fetch_kamis_prices
34
  from src.ingestion.nasa_power import fetch_all_mandis_nasa_power, DailyReading
35
  from src.extraction.agent import ExtractionAgent, RuleBasedExtractor
36
  from src.reconciliation.agent import ReconciliationAgent, RuleBasedReconciler
 
228
 
229
  try:
230
  async def _ingest_agmarknet():
231
+ # Under Kenya, the primary wholesale source is KAMIS (no eNAM).
232
+ if REGION == "kenya":
233
+ try:
234
+ self._agmarknet_prices = await fetch_kamis_prices(
235
+ MANDIS, COMMODITIES, days_back=self.days_back,
236
+ )
237
+ except Exception as e:
238
+ errors.append(f"KAMIS fetch failed: {e}")
239
+ return
240
  try:
241
  self._agmarknet_prices = await fetch_mandi_prices(
242
  MANDIS, COMMODITIES, days_back=self.days_back,
 
245
  errors.append(f"Agmarknet fetch failed: {e}")
246
 
247
  async def _ingest_enam():
248
+ if REGION == "kenya":
249
+ self._enam_prices = {m.mandi_id: [] for m in MANDIS}
250
+ return
251
  try:
252
  self._enam_prices = await fetch_enam_prices(
253
  MANDIS, COMMODITIES, days_back=min(14, self.days_back),
src/rag/knowledge_base_kenya.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Kenya agricultural marketing knowledge base for RAG retrieval.
3
+
4
+ ~25 knowledge chunks covering bimodal crop calendars, NCPB/KAMIS/county
5
+ institutions, post-harvest handling (aflatoxin, PICS, metal silos),
6
+ transport (A-roads vs feeder roads, Mombasa corridor), storage
7
+ (NCPB depots, CACs, warehouse receipts), and shocks (2022 Horn of
8
+ Africa drought, 2023-24 El Nino floods, IPC classifications).
9
+
10
+ Used by the recommendation agent when REGION=kenya. Sibling to
11
+ knowledge_base.py (Tamil Nadu / India). Schema matches the India
12
+ module exactly: KnowledgeChunk dataclass with id/title/source/category/text.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+
19
+
20
+ @dataclass
21
+ class KnowledgeChunk:
22
+ id: str
23
+ title: str
24
+ source: str
25
+ category: str
26
+ text: str
27
+
28
+
29
+ KNOWLEDGE_CHUNKS: list[KnowledgeChunk] = [
30
+
31
+ # -- Crop Calendars -------------------------------------------------
32
+
33
+ KnowledgeChunk(
34
+ "CC-001", "Maize Long Rains Calendar - Rift Valley and Western Kenya",
35
+ "MoALF Kenya Crop Production Guidelines 2023; FEWS NET Kenya Seasonal Monitor",
36
+ "crop_calendar",
37
+ "The Long Rains are Kenya's primary cropping season and produce roughly 70-80% of the national maize crop. "
38
+ "Land preparation begins Feb-Mar, planting Mar-May as rains establish, vegetative growth Apr-Jun, "
39
+ "and harvest Jul-Sep in most zones. Main producing counties: Trans-Nzoia, Uasin Gishu, Nakuru, Narok, "
40
+ "Bungoma, Kakamega, Nyandarua. Rift Valley high-altitude zones (Trans-Nzoia, Uasin Gishu) harvest "
41
+ "latest (Sep-Nov) because of longer maturation at altitude. Wholesale maize price troughs typically "
42
+ "arrive Aug-Oct when Western and Rift Valley grain floods Nairobi, Kisumu, Eldoret, and Nakuru markets."
43
+ ),
44
+ KnowledgeChunk(
45
+ "CC-002", "Maize Short Rains Calendar - Eastern and Coastal Kenya",
46
+ "MoALF Kenya Crop Production Guidelines 2023; KALRO Maize Agronomy Manual",
47
+ "crop_calendar",
48
+ "The Short Rains (Oct-Dec) are the secondary season and dominate Eastern, lower Central, and Coastal "
49
+ "Kenya, where a single unimodal rainy season is unreliable. Planting Oct-Nov, harvest Jan-Feb. "
50
+ "Key counties: Machakos, Makueni, Kitui, Embu, Tharaka-Nithi, Kilifi, Kwale. Short Rains maize is "
51
+ "far more vulnerable to dry spells -- the 2022 consecutive failure of five rainy seasons devastated "
52
+ "this region. Short Rains harvest pushes a secondary price trough in Feb-Mar in Machakos, Mwingi, "
53
+ "and Wote markets, though volumes are a fraction of the Long Rains crop."
54
+ ),
55
+ KnowledgeChunk(
56
+ "CC-003", "Maize Moisture and Safe-Storage Thresholds",
57
+ "KALRO Post-Harvest Handbook; FAO Kenya Aflatoxin Mitigation Guide",
58
+ "crop_calendar",
59
+ "Maize at harvest is typically 18-25% moisture and must be dried to 13.5% or below before storage. "
60
+ "Above 13.5% moisture, Aspergillus flavus growth accelerates and aflatoxin contamination risk rises "
61
+ "sharply; above 18% grain may rot within weeks. Sun-drying on raised tarpaulins or drying cribs over "
62
+ "5-10 days is standard for smallholders; avoid drying on bare ground (soil contamination and re-wetting). "
63
+ "NCPB rejects deliveries above 13.5% moisture. Traders discount roughly KES 100-200 per 90kg bag for "
64
+ "each percentage point above 13.5%, and a heavily aflatoxin-positive lot may be unsellable entirely."
65
+ ),
66
+ KnowledgeChunk(
67
+ "CC-004", "Bean Crop Calendar - Bimodal Smallholder Staple",
68
+ "KALRO Legumes Research Programme; MoALF Pulses Strategy 2020-2025",
69
+ "crop_calendar",
70
+ "Beans (Rosecoco, Mwitemania, Mwezi Moja, Wairimu) are grown in both rainy seasons on 75-90 day cycles, "
71
+ "shorter than maize, and are frequently intercropped with maize. Long Rains: plant Mar-Apr, harvest "
72
+ "Jun-Jul. Short Rains: plant Oct-Nov, harvest Jan-Feb. Main counties: Nakuru, Narok, Bomet, Kirinyaga, "
73
+ "Meru, Embu, Machakos. Beans are drought-sensitive at flowering (days 35-45); mid-season dry spells "
74
+ "routinely cut yields 30-50%. Post-harvest beans must be dried to <13% moisture; bruchid weevils are "
75
+ "the primary storage pest and destroy improperly-stored lots within 2-3 months."
76
+ ),
77
+ KnowledgeChunk(
78
+ "CC-005", "Irish Potato Calendar - Nyandarua, Meru, and Nakuru",
79
+ "National Potato Council of Kenya; KALRO Tigoni",
80
+ "crop_calendar",
81
+ "Irish potato (Shangi, Dutch Robijn, Asante) is grown at 1,800-3,000m altitude on a 3-4 month cycle "
82
+ "tied to the rains. Long Rains planting Mar-Apr, harvest Jun-Aug. Short Rains planting Oct-Nov, "
83
+ "harvest Feb-Mar. Production is concentrated in Nyandarua (~40% of national output), Meru, Elgeyo "
84
+ "Marakwet, Nakuru, Bomet, and Bungoma. Shangi dominates (~80% of market) because it ripens quickly "
85
+ "and handles transport, but it has very short dormancy (2-3 weeks) -- farmers cannot hold crop, "
86
+ "which drives distress selling at harvest and extreme price swings. Nyandarua farm-gate prices have "
87
+ "swung from KES 800 to KES 3,500 per 110kg bag within a single season."
88
+ ),
89
+
90
+ # -- Market Institutions --------------------------------------------
91
+
92
+ KnowledgeChunk(
93
+ "MI-001", "NCPB - National Cereals and Produce Board",
94
+ "NCPB Annual Reports; MoALF Strategic Food Reserve Policy",
95
+ "market_institution",
96
+ "NCPB is Kenya's parastatal grain buyer of last resort and operator of the Strategic Food Reserve. "
97
+ "When activated by the Cabinet Secretary, NCPB offers a Guaranteed Minimum Return Price (GMRP) for "
98
+ "maize -- recent GMRP levels have ranged KES 3,000-5,200 per 90kg bag, well above distress-sale "
99
+ "farm-gate levels in surplus seasons. NCPB operates ~110 depots nationwide with concentration in "
100
+ "Rift Valley and Western counties. Payment delays of 30-90 days are common; smallholders need "
101
+ "registration, ID, and land documentation. Intake quality standards: <13.5% moisture, <3% foreign "
102
+ "matter, aflatoxin within codex limits. When GMRP is not activated, NCPB buys at market rates or not at all."
103
+ ),
104
+ KnowledgeChunk(
105
+ "MI-002", "KAMIS - Kenya Agricultural Market Information System",
106
+ "KAMIS Portal; MoALF Market Information Unit",
107
+ "market_institution",
108
+ "KAMIS is the MoALF-run market information service (kamis.kilimo.go.ke) that publishes daily, weekly, "
109
+ "and monthly wholesale prices for 40+ commodities across 60+ markets nationwide. Coverage is strongest "
110
+ "for Nairobi (Wakulima, Kangemi, Gikomba), Mombasa (Kongowea), Eldoret, Kisumu (Kibuye), and Nakuru. "
111
+ "Daily prices reflect wholesale trades and typically sit 15-40% above farm-gate. KAMIS is the canonical "
112
+ "source for price reconciliation and for retrospective benchmark panels. Gaps: farm-gate prices are "
113
+ "not systematically collected, and rural market coverage thins outside the main trunk corridors."
114
+ ),
115
+ KnowledgeChunk(
116
+ "MI-003", "County Governments Post-2013 Devolution",
117
+ "Constitution of Kenya 2010, Fourth Schedule; Kenya Devolution Working Paper (World Bank 2020)",
118
+ "market_institution",
119
+ "Agriculture is a devolved function under Kenya's 2010 Constitution -- each of the 47 counties runs "
120
+ "its own Department of Agriculture with budgets for extension, input subsidies (often fertilizer and "
121
+ "certified seed), market infrastructure, and cooperative support. Practical consequences for farmers: "
122
+ "input subsidy program eligibility, cooperative registration, and county market fees all vary by county. "
123
+ "Meru and Nyandarua run active potato-sector programs; Kakamega and Bungoma run maize-and-dairy extension; "
124
+ "ASAL counties (Kitui, Makueni, Turkana) focus on drought-tolerant sorghum and millet. National-level "
125
+ "MoALF still sets price-support policy (NCPB, GMRP) and runs KAMIS."
126
+ ),
127
+ KnowledgeChunk(
128
+ "MI-004", "SACCOs and Cooperatives for Rural Finance and Aggregation",
129
+ "SASRA Annual Supervision Report; Co-operative Bank of Kenya Agri-SACCO Study",
130
+ "market_institution",
131
+ "SACCOs (Savings and Credit Cooperatives) are the backbone of Kenyan rural finance. Regulated by "
132
+ "SASRA, farmer SACCOs pool member savings, provide input credit (fertilizer, seed on 3-6 month terms), "
133
+ "and aggregate produce for bulk sale. Agricultural SACCOs are strongest in coffee (Mt. Kenya), dairy "
134
+ "(Rift Valley), and tea (Kericho, Nandi) zones; maize and bean cooperatives are weaker but growing. "
135
+ "Typical benefits of SACCO membership: input credit at 12-15% annual (vs. 25%+ from informal lenders), "
136
+ "shared transport to terminal markets, and access to NCPB delivery letters. Dormant and weak-governance "
137
+ "cooperatives are common -- farmers should verify SASRA registration before committing savings."
138
+ ),
139
+
140
+ # -- Post-Harvest Handling -----------------------------------------
141
+
142
+ KnowledgeChunk(
143
+ "PH-001", "Aflatoxin Risk in Kenya - Regions and History",
144
+ "Kenya Ministry of Health Aflatoxin Surveillance; Lewis et al. 2005 (2004 Machakos Outbreak)",
145
+ "post_harvest",
146
+ "Aflatoxin contamination is Kenya's most severe post-harvest food-safety risk, caused by Aspergillus "
147
+ "flavus on improperly-dried maize. The 2004 outbreak centered on Makueni and Machakos killed 125+ "
148
+ "people from acute aflatoxicosis -- still among the largest aflatoxin mortality events ever recorded. "
149
+ "High-risk zones: Eastern Kenya lower midlands (Machakos, Makueni, Kitui, Embu, Tharaka-Nithi) where "
150
+ "Short Rains harvests coincide with lingering humidity, and coastal Kenya (Kilifi, Kwale, Taita-Taveta) "
151
+ "year-round. Rift Valley Long Rains crops are lower-risk because harvest falls in the drier Jul-Sep "
152
+ "window. Aflasafe KE01 biocontrol is available through KALRO and reduces field contamination 80-90% "
153
+ "when applied 2-3 weeks before flowering."
154
+ ),
155
+ KnowledgeChunk(
156
+ "PH-002", "PICS Bags - Hermetic Triple-Layer Storage",
157
+ "Purdue Improved Crop Storage (PICS) Project; KENAFF Distribution Records",
158
+ "post_harvest",
159
+ "PICS (Purdue Improved Crop Storage) bags are triple-layer hermetic storage sacks -- two polyethylene "
160
+ "liners inside a woven polypropylene outer bag -- that suffocate storage pests (maize weevil, larger "
161
+ "grain borer, bruchid) within 10-14 days of sealing, without any chemicals. A 90kg PICS bag costs "
162
+ "KES 250-400 in Kenyan agrovet shops (2024-2025), reusable 2-3 seasons. Storage loss drops from "
163
+ "20-30% over 6 months (open-weave gunny bags + actellic dusting) to under 2% with PICS. Distribution "
164
+ "in Kenya runs through KENAFF (Kenya National Farmers' Federation), KALRO, and private agrovet chains "
165
+ "(Farmers Choice, Pwani, Amiran). Adoption is highest in Western Kenya and Rift Valley."
166
+ ),
167
+ KnowledgeChunk(
168
+ "PH-003", "Metal Silo Program - CIMMYT, KALRO, EGSP",
169
+ "CIMMYT Effective Grain Storage Project (EGSP) 2008-2016; KALRO Post-Harvest Unit",
170
+ "post_harvest",
171
+ "Metal silos (galvanized sheet steel, 180-3,000kg capacity) are the premium on-farm storage option, "
172
+ "disseminated in Kenya through the CIMMYT-led Effective Grain Storage Project with KALRO and the "
173
+ "Ministry of Agriculture (2008-2016). A 900kg silo costs KES 18,000-30,000 through trained artisans "
174
+ "in Nakuru, Bungoma, and Embu. Storage losses drop to near zero over 12+ months. Economics work for "
175
+ "farmers with >10 bags to store who want to capture the Oct-trough to Mar-peak maize price cycle "
176
+ "(15-25% seasonal spread). Adoption remains limited by upfront cost; SACCOs and county agriculture "
177
+ "departments occasionally subsidize silos for smallholder cooperatives."
178
+ ),
179
+ KnowledgeChunk(
180
+ "PH-004", "WFP and FAO Post-Harvest Programs (P4P and Successors)",
181
+ "WFP Kenya Country Strategic Plan; FAO Kenya Resilience Programme",
182
+ "post_harvest",
183
+ "WFP's Purchase for Progress (P4P, 2008-2014) demonstrated smallholder aggregation into WFP's Kenya "
184
+ "food-aid supply chain, sourcing maize and pulses from producer organizations at prices above farm-gate "
185
+ "in exchange for quality and volume commitments. P4P successors under WFP Country Strategic Plans "
186
+ "continue smallholder sourcing in Bungoma, Busia, Makueni, Kitui, and Trans-Nzoia. FAO's Kenya "
187
+ "Resilience and Food Systems programmes fund post-harvest training, hermetic storage subsidies, and "
188
+ "aflatoxin testing equipment at county level. Participating farmer groups typically see 20-30% price "
189
+ "uplift on the sold portion plus training in quality grading and moisture testing."
190
+ ),
191
+
192
+ # -- Transport ------------------------------------------------------
193
+
194
+ KnowledgeChunk(
195
+ "TR-001", "Kenya Road Network - Classes A, B, C and Corridor Geography",
196
+ "Kenya National Highways Authority (KeNHA); Kenya Rural Roads Authority (KeRRA)",
197
+ "transport",
198
+ "Kenya's classified road network has roughly 161,000 km, of which only ~14,000 km is paved. "
199
+ "Class A roads are international trunk corridors (A1 Malaba-Kisumu-Kisii-Isebania west corridor and "
200
+ "A109 Nairobi-Mombasa, the Northern Corridor). Class B are national trunks linking regions. "
201
+ "Class C are primary feeder roads. Classes D and E are rural feeders, almost entirely unpaved and "
202
+ "often impassable in heavy rain. Almost all maize moves on paved A/B roads once aggregated, but the "
203
+ "first 5-50 km from farm to market centre is on D/E feeders -- this is where weather disruption, "
204
+ "broken bridges, and seasonal impassability hit smallholders hardest."
205
+ ),
206
+ KnowledgeChunk(
207
+ "TR-002", "Transport Cost Deltas - Paved vs Unpaved",
208
+ "Kenya Transport Sector Study (World Bank 2022); KENHA Haulage Cost Schedules",
209
+ "transport",
210
+ "Typical truckage costs (2024) for maize and produce: Paved trunk road (A/B), 30-tonne truck: "
211
+ "KES 7-12 per tonne-km. Gravel/unpaved feeder road: KES 15-25 per tonne-km, sometimes higher in wet "
212
+ "season because of reduced load factors and longer transit time. Small-vehicle rates: pickup (1-2t) "
213
+ "KES 25-40 per tonne-km; matatu-sized 7t lorries KES 15-20 per tonne-km. Farmers selling 5-20 bags "
214
+ "typically cannot fill a full truck and pay a premium for shared or piecemeal transport. Concrete "
215
+ "example: Trans-Nzoia to Nairobi (~380 km paved trunk) runs ~KES 180-220 per 90kg bag on a full lorry; "
216
+ "the final 15 km from farm to Kitale collection point can cost another KES 40-80 per bag."
217
+ ),
218
+ KnowledgeChunk(
219
+ "TR-003", "Aggregators, Brokers, and Farmer Transport Options",
220
+ "Tegemeo Institute Smallholder Market Access Study 2021",
221
+ "transport",
222
+ "Most Kenyan smallholders sell to itinerant middlemen (brokers, 'madalali') who bring lorries or "
223
+ "pickups to the farm gate or village market. Broker margins are typically 10-25% above farm-gate to "
224
+ "wholesale-market prices, reflecting risk, financing, and transport. Alternatives for farmers: "
225
+ "deliver to a county NCPB depot (requires registration and the depot being open for GMRP intake), "
226
+ "aggregate through a SACCO or cooperative for shared truckage, or use a matatu/boda-boda for very "
227
+ "small volumes to the nearest market centre. The smaller the volume, the weaker the farmer's "
228
+ "bargaining position -- sub-10-bag smallholders routinely accept 20-35% below the KAMIS wholesale "
229
+ "price reported for the nearest town."
230
+ ),
231
+ KnowledgeChunk(
232
+ "TR-004", "Mombasa Port Bottlenecks and Seasonal Congestion",
233
+ "Kenya Ports Authority Performance Reports; Shippers Council of Eastern Africa",
234
+ "transport",
235
+ "The Nairobi-Mombasa A109 corridor is Kenya's transport spine. Mombasa Port handles imports for the "
236
+ "entire Northern Corridor (including landlocked Uganda, Rwanda, South Sudan, eastern DRC), which "
237
+ "competes with outbound agricultural freight for the same truck fleet. Peak congestion windows: "
238
+ "Nov-Jan (holiday consumer imports) and Mar-May (fertilizer and agro-input imports before Long Rains) "
239
+ "push per-tonne-km haulage rates up 15-30%. The 2023-24 El Nino floods cut the A109 at multiple "
240
+ "points (including the long-running Mtito Andei washout) and diverted freight for weeks. SGR "
241
+ "(Standard Gauge Railway) handles containerized freight but has limited relevance for bulk "
242
+ "agricultural produce moving on short domestic hops."
243
+ ),
244
+ KnowledgeChunk(
245
+ "TR-005", "County Road Quality - Nyandarua Potato Case",
246
+ "Nyandarua County Integrated Development Plan; National Potato Council of Kenya",
247
+ "transport",
248
+ "Road quality varies dramatically by county and is a first-order determinant of farm-gate price. "
249
+ "Nyandarua -- Kenya's largest potato producer -- is notorious for poor feeder roads connecting "
250
+ "Kinangop, Ol Kalou, and Ndaragwa to the tarmac network. In the Mar-May Long Rains, Nyandarua feeder "
251
+ "roads become impassable for days at a time, trapping potato harvests at the farm and forcing distress "
252
+ "sales to brokers who own 4WD trucks at steep discounts. By contrast, Meru and Nakuru potato zones "
253
+ "have better C-class road coverage and farm-gate to wholesale price spreads are 15-20 percentage "
254
+ "points narrower. Farmers and aggregators in rain-affected counties should plan sales windows around "
255
+ "forecast dry spells."
256
+ ),
257
+
258
+ # -- Storage --------------------------------------------------------
259
+
260
+ KnowledgeChunk(
261
+ "ST-001", "NCPB Depot Storage - Access, Fees, Capacity",
262
+ "NCPB Operations Manual; Kenya Grain Council Storage Directory",
263
+ "storage",
264
+ "NCPB operates ~110 depots with combined storage capacity around 2.0 million bags (180,000 MT) of "
265
+ "grain. Beyond buyer-of-last-resort procurement, NCPB also rents storage to traders and farmer "
266
+ "groups at KES 8-15 per 90kg bag per month (varies by depot and season). Access for smallholders "
267
+ "is mediated by registration requirements (national ID, KRA PIN, cooperative or group membership). "
268
+ "Major depots: Eldoret, Kitale, Bungoma, Nakuru, Meru, Nairobi Industrial Area. Smallholder "
269
+ "deposits have to meet intake quality standards (<13.5% moisture, clean). Warehouse-receipt-backed "
270
+ "lending against NCPB stock exists but is limited in practice."
271
+ ),
272
+ KnowledgeChunk(
273
+ "ST-002", "Community Aggregation Centres (CACs)",
274
+ "AGRA Kenya Smallholder Aggregation Review; MoALF Cooperative Sub-Sector Report",
275
+ "storage",
276
+ "Community Aggregation Centres are smallholder-focused collection and short-term storage facilities, "
277
+ "typically county-government-built or AGRA/NGO-funded structures of 50-500 MT capacity. CACs allow "
278
+ "farmers to pool grain, share drying floors and moisture meters, and negotiate as a unit with traders "
279
+ "or with NCPB. Strongest CAC networks are in Bungoma, Busia, Trans-Nzoia, and Meru. Storage fees are "
280
+ "nominal (KES 5-10 per bag per month) but capacity is limited and governance is uneven -- some CACs "
281
+ "are effectively dormant. CAC membership typically provides the bridge smallholders need to access "
282
+ "the warehouse receipt system or direct NCPB delivery."
283
+ ),
284
+ KnowledgeChunk(
285
+ "ST-003", "Warehouse Receipt System - KACE and WRC",
286
+ "Kenya Agricultural Commodity Exchange (KACE); Warehouse Receipt System Council Act 2019",
287
+ "storage",
288
+ "Kenya's Warehouse Receipt System, formalized under the Warehouse Receipt System Council Act 2019, "
289
+ "remains nascent relative to more mature counterparts (e.g., Zambia, Uganda). KACE (Kenya Agricultural "
290
+ "Commodity Exchange) pioneered receipt-based trading and price-discovery bulletin services, and a "
291
+ "small network of licensed warehouses issues negotiable receipts usable as loan collateral. Typical "
292
+ "all-in cost (storage + fumigation + receipt fees): KES 40-80 per 90kg bag per month. Pledge-loan "
293
+ "advance against receipt: 50-70% of market value at 12-15% annual. System is most functional for "
294
+ "maize in Rift Valley warehouses; coverage for pulses, sorghum, and perishables is thin."
295
+ ),
296
+
297
+ # -- Shocks ---------------------------------------------------------
298
+
299
+ KnowledgeChunk(
300
+ "SH-001", "2020-2023 Horn of Africa Drought - Five Failed Rainy Seasons",
301
+ "FEWS NET Horn of Africa Special Report 2023; WFP VAM Kenya Drought Impact Assessment",
302
+ "shock",
303
+ "The 2020-2023 Horn of Africa drought was the longest and most severe drought in the region since "
304
+ "at least 1981, defined by five consecutive below-average rainy seasons (2020 Short Rains through "
305
+ "2022 Short Rains). In Kenya the ASAL counties -- Turkana, Marsabit, Mandera, Wajir, Garissa, "
306
+ "Isiolo, Samburu, Tana River, Kitui, Makueni -- bore the brunt, with ~4.4 million people food-"
307
+ "insecure at the 2022 peak. Maize wholesale prices in affected northern and eastern markets "
308
+ "(Garissa, Wajir, Mandera, Kitui, Marsabit) spiked 60-100% above five-year averages. Short Rains "
309
+ "2023 brought partial recovery but the humanitarian caseload remained elevated into 2024. The "
310
+ "drought is the signature shock in the first half of the LastMileBench Kenya benchmark window."
311
+ ),
312
+ KnowledgeChunk(
313
+ "SH-002", "2023-24 El Nino Floods - October Onwards",
314
+ "Kenya Red Cross Situation Reports 2023-24; World Weather Attribution El Nino Kenya Analysis",
315
+ "shock",
316
+ "The Short Rains 2023 season (Oct-Dec) delivered an El Nino-driven above-average rainfall that "
317
+ "tipped into widespread flooding from late October 2023 through April 2024. Impacts: Tana River "
318
+ "overflow and dam spillover (Garsen, Tana Delta), landslide-driven road closures in Rift Valley "
319
+ "(Elgeyo Marakwet, West Pokot), inundated maize-harvest areas in lower Eastern Kenya just as Short "
320
+ "Rains crops were being brought in, and multiple cuts to the A109 Nairobi-Mombasa highway. More "
321
+ "than 300 deaths and 500,000 displaced at peak in April-May 2024. Maize transit times from "
322
+ "Trans-Nzoia and Uasin Gishu to Nairobi lengthened by 30-60% for weeks, widening producer-to-"
323
+ "wholesale spreads. This is the second signature shock in the LastMileBench Kenya benchmark window."
324
+ ),
325
+ KnowledgeChunk(
326
+ "SH-003", "IPC/FEWS NET Food Security Classifications for Kenya",
327
+ "IPC Global Partners; FEWS NET Kenya Food Security Outlook (https://fews.net/east-africa/kenya)",
328
+ "shock",
329
+ "FEWS NET and the IPC (Integrated Food Security Phase Classification) use a five-phase scale: "
330
+ "1-Minimal, 2-Stressed, 3-Crisis, 4-Emergency, 5-Famine. Kenya's ASAL counties have been chronically "
331
+ "in Phase 2 (Stressed) or Phase 3 (Crisis) since the mid-2000s, with brief escalations to Phase 4 "
332
+ "(Emergency) during acute drought peaks -- including much of 2022 across northern and eastern "
333
+ "counties. Agricultural high-potential zones (Rift Valley, Western, Central) typically sit in Phase 1. "
334
+ "FEWS NET publishes a Kenya Food Security Outlook every four months with updated county-level maps, "
335
+ "the canonical reference for cross-regional food-security context in the benchmark panel."
336
+ ),
337
+ ]
338
+
339
+ KNOWLEDGE_BASE = KNOWLEDGE_CHUNKS
src/rag/provider.py CHANGED
@@ -12,7 +12,12 @@ from __future__ import annotations
12
  import logging
13
  from typing import Any
14
 
15
- from src.rag.knowledge_base import KNOWLEDGE_BASE, KnowledgeChunk
 
 
 
 
 
16
 
17
  log = logging.getLogger(__name__)
18
 
 
12
  import logging
13
  from typing import Any
14
 
15
+ from config import REGION
16
+
17
+ if REGION == "kenya":
18
+ from src.rag.knowledge_base_kenya import KNOWLEDGE_BASE, KnowledgeChunk
19
+ else:
20
+ from src.rag.knowledge_base import KNOWLEDGE_BASE, KnowledgeChunk
21
 
22
  log = logging.getLogger(__name__)
23
 
tests/fixtures/kamis_nakuru_dry_maize_sample.html ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>KAMIS</title>
7
+ <!-- Global site tag (gtag.js) - Google Analytics -->
8
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-LVPBH4Z6YM"></script>
9
+ <script>
10
+ window.dataLayer = window.dataLayer || [];
11
+ function gtag(){dataLayer.push(arguments);}
12
+ gtag('js', new Date());
13
+
14
+ gtag('config', 'G-LVPBH4Z6YM');
15
+ </script>
16
+ <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Abril+Fatface" />
17
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous" />
18
+ <link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
19
+ <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
20
+ <link href="https://kamis.kilimo.go.ke/resources/frontpage-vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
21
+ <link href="https://kamis.kilimo.go.ke/resources/frontpage-vendor/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
22
+ <link href="https://kamis.kilimo.go.ke/resources/frontpage-vendor/aos/aos.css" rel="stylesheet">
23
+ <link href="https://kamis.kilimo.go.ke/resources/frontpage-vendor/remixicon/remixicon.css?v=1.123" rel="stylesheet">
24
+ <link href="https://kamis.kilimo.go.ke/resources/frontpage-vendor/swiper/swiper-bundle.min.css" rel="stylesheet">
25
+ <link href="https://kamis.kilimo.go.ke/resources/frontpage-vendor/glightbox/css/glightbox.min.css" rel="stylesheet">
26
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/general.css?v=1.125" rel="stylesheet">
27
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/sections.css?v=1.123" rel="stylesheet">
28
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/breadcrumbs.css?v=1.123" rel="stylesheet">
29
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/back-to-top.css?v=1.123" rel="stylesheet">
30
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/header.css?v=1.123" rel="stylesheet">
31
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/navigation-menu.css?v=1.123" rel="stylesheet">
32
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/hero-section.css?v=1.123" rel="stylesheet">
33
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/about.css?v=1.123" rel="stylesheet">
34
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/values.css?v=1.123" rel="stylesheet">
35
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/counts.css?v=1.123" rel="stylesheet">
36
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/features.css?v=1.123" rel="stylesheet">
37
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/services.css?v=1.123" rel="stylesheet">
38
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/pricing.css?v=1.123" rel="stylesheet">
39
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/faq.css?v=1.123" rel="stylesheet">
40
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/counts.css?v=1.123" rel="stylesheet">
41
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/features.css?v=1.123" rel="stylesheet">
42
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/services.css?v=1.123" rel="stylesheet">
43
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/pricing.css?v=1.123" rel="stylesheet">
44
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/faq.css?v=1.123" rel="stylesheet">
45
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/portfolio.css?v=1.123" rel="stylesheet">
46
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/testimonials.css?v=1.123" rel="stylesheet">
47
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/team.css?v=1.123" rel="stylesheet">
48
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/clients.css?v=1.123" rel="stylesheet">
49
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/blog.css?v=1.123" rel="stylesheet">
50
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/contact.css?v=1.123" rel="stylesheet">
51
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/product-item.css?v=1.123" rel="stylesheet">
52
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/footer.css?v=1.124" rel="stylesheet">
53
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/price-watch.css?v=1.123" rel="stylesheet">
54
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/latest-reports.css?v=1.123" rel="stylesheet">
55
+ <link href="https://kamis.kilimo.go.ke/resources/css/frontpage/highchart.css?v=1.123" rel="stylesheet">
56
+ <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.24/css/jquery.dataTables.css">
57
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
58
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Zebra_datepicker/1.9.18/css/bootstrap/zebra_datepicker.css" integrity="sha512-jGnP90W9TO7rAXE8dkl8c91DsyVxOKkMP7+fvYGW90qfp5QfHwinUADn4K+jyhDKdynKbS6DR+1UAiEyFl1dcQ==" crossorigin="anonymous" />
59
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
60
+ integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
61
+ crossorigin="anonymous"></script>
62
+ <script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.js"></script>
63
+ <script src="https://code.highcharts.com/highcharts.js"></script>
64
+ <script src="https://code.highcharts.com/modules/series-label.js"></script>
65
+ <script src="https://code.highcharts.com/modules/exporting.js"></script>
66
+ <script src="https://code.highcharts.com/modules/export-data.js"></script>
67
+ <script src="https://code.highcharts.com/modules/accessibility.js"></script>
68
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js"></script>
69
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" rel="stylesheet" />
70
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Zebra_datepicker/1.9.18/zebra_datepicker.min.js" integrity="sha512-+jF6u7aOmduPkX9JBkvp1B5XgGqvfc3fPGWJG43Ci18tZ4hR1jgHMrlWrM8ai73KzuMc6hvp+9S2k9ggGKYTiw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
71
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Zebra_datepicker/1.9.18/css/bootstrap/zebra_datepicker.css" integrity="sha512-jGnP90W9TO7rAXE8dkl8c91DsyVxOKkMP7+fvYGW90qfp5QfHwinUADn4K+jyhDKdynKbS6DR+1UAiEyFl1dcQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
72
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
73
+ <meta name="description" content="KAMIS" />
74
+ <meta name="keywords" content="market, prices" />
75
+ <meta name="author" content="KAMIS" /> </head>
76
+ <body>
77
+ <header id="header" class="header fixed-top" style="padding: 0.4em;">
78
+
79
+ <div class="container container-xl d-flex align-items-center justify-content-between mt-2">
80
+ <a href="https://kamis.kilimo.go.ke/" class="logo d-flex align-items-center">
81
+ <img src="https://kamis.kilimo.go.ke/resources/frontpage-images/logo.jpg" alt="logo">
82
+ </a>
83
+ <nav id="navbar" class="navbar">
84
+ <ul>
85
+ <!--<li class=""><a class="nav-link scrollto active" href="https://kamis.kilimo.go.ke/">HOME</a></li>-->
86
+ <li class=""><a class="nav-link scrollto" href="https://kamis.kilimo.go.ke/">HOME</a></li>
87
+ <li class=""><a class="nav-link" href="https://kamis.kilimo.go.ke/index.php/site/about">ABOUT</a></li>
88
+ <li class="dropdown"><a><span>MARKETS</span></a>
89
+ <ul>
90
+ <li><a href="https://kamis.kilimo.go.ke/site/market">Commodity Prices</a></li>
91
+ <li><a href="https://kamis.kilimo.go.ke/site/market_search">Price Search</a></li>
92
+ </ul>
93
+ </li>
94
+ <li class="dropdown"><a><span>CROSS-BORDER</span></a>
95
+ <ul>
96
+ <li><a href="https://kamis.kilimo.go.ke/site/border">Trade Volumes</a></li>
97
+ <li><a href="https://kamis.kilimo.go.ke/site/border_search">Volume Search</a></li>
98
+ </ul>
99
+ </li>
100
+ </ul>
101
+ <i class="bi bi-list mobile-nav-toggle"></i>
102
+ </nav>
103
+ </div> </header>
104
+ <section id="body-section" class="mt-3" >
105
+ <div class="container">
106
+ <div class="row">
107
+ <div class="col-lg-12">
108
+ <h1 class="page-header" style="font-size: 1.5rem;">Market
109
+ <small>prices</small>
110
+ </h1>
111
+ <ol class="breadcrumb">
112
+ <li><a href="https://kamis.kilimo.go.ke/">Home</a></li>
113
+ |
114
+ <li class="active">Market Search</li>
115
+ </ol>
116
+ </div>
117
+ </div>
118
+ <div class="row">
119
+ <div class="col-lg-12">
120
+ <div class="panel panel-default">
121
+ <div class="panel-body">
122
+ <div class="card bg-light">
123
+ <div class="card-body">
124
+ <form role="form" action="market_search" method="get">
125
+ <div class="row">
126
+ <div class="col-md-4">
127
+ <div class="form-group">
128
+ <label>County</label>
129
+ <select name="county[]" class="form-control" id="selCounty" multiple="multiple">
130
+ <option value="">--Please select--</option>
131
+ <option value="Nairobi">Nairobi</option>
132
+ <option value="Machakos">Machakos</option>
133
+ <option value="Uasin-gishu">Uasin-gishu</option>
134
+ <option value="Mombasa">Mombasa</option>
135
+ <option value="Embu">Embu</option>
136
+ <option value="Isiolo">Isiolo</option>
137
+ <option value="Kitui">Kitui</option>
138
+ <option value="Nakuru">Nakuru</option>
139
+ <option value="Kakamega">Kakamega</option>
140
+ <option value="Kisumu">Kisumu</option>
141
+ <option value="Busia">Busia</option>
142
+ <option value="Migori">Migori</option>
143
+ <option value="Marsabit">Marsabit</option>
144
+ <option value="Kajiado">Kajiado</option>
145
+ <option value="Taveta">Taveta</option>
146
+ <option value="Kwale">Kwale</option>
147
+ <option value="Trans-nzoia">Trans-nzoia</option>
148
+ <option value="Bomet">Bomet</option>
149
+ <option value="Garissa">Garissa</option>
150
+ <option value="Muranga">Muranga</option>
151
+ <option value="Nyandarua">Nyandarua</option>
152
+ <option value="Wajir">Wajir</option>
153
+ <option value="Meru">Meru</option>
154
+ <option value="Narok">Narok</option>
155
+ <option value="Kericho">Kericho</option>
156
+ <option value="Nyamira">Nyamira</option>
157
+ <option value="Taita-taveta">Taita-taveta</option>
158
+ <option value="Lamu">Lamu</option>
159
+ <option value="Tana-river">Tana-river</option>
160
+ <option value="Kilifi">Kilifi</option>
161
+ <option value="Kiambu">Kiambu</option>
162
+ <option value="Nyeri">Nyeri</option>
163
+ <option value="Elgeyo-marakwet">Elgeyo-marakwet</option>
164
+ <option value="West-pokot">West-pokot</option>
165
+ <option value="Samburu">Samburu</option>
166
+ <option value="Laikipia">Laikipia</option>
167
+ <option value="Baringo">Baringo</option>
168
+ <option value="Mandera">Mandera</option>
169
+ <option value="Makueni">Makueni</option>
170
+ <option value="Tharaka-nithi">Tharaka-nithi</option>
171
+ <option value="Kirinyaga">Kirinyaga</option>
172
+ <option value="Vihiga">Vihiga</option>
173
+ <option value="Siaya">Siaya</option>
174
+ <option value="Nandi">Nandi</option>
175
+ <option value="Homa-bay">Homa-bay</option>
176
+ <option value="Kisii">Kisii</option>
177
+ <option value="Turkana">Turkana</option>
178
+ <option value="Test">Test</option>
179
+ <option value="Bungoma">Bungoma</option>
180
+ </select>
181
+ </div>
182
+ </div>
183
+ <div class="col-md-4">
184
+ <div class="form-group">
185
+ <label>Markets</label>
186
+ <select name="market[]" class="multiple-select form-control" id="selMarket" multiple="multiple">
187
+ <option value="">--Please select--</option>
188
+ </select>
189
+ </div>
190
+ </div>
191
+ <div class="col-md-4">
192
+ <div class="form-group">
193
+ <label>Product</label>
194
+ <select name="product[]" class="multiple-select form-control" id="selProduct" multiple="multiple">
195
+ <option value="">--Please select--</option>
196
+ <option value="1">Dry maize</option>
197
+ <option value="2">Red sorghum</option>
198
+ <option value="3">Wheat</option>
199
+ <option value="4">Rice</option>
200
+ <option value="10">Green grams</option>
201
+ <option value="12">Ground nuts</option>
202
+ <option value="29">Beans red haricot (wairimu)</option>
203
+ <option value="30">Beans (yellow-green)</option>
204
+ <option value="50">Dolichos lablab (njahi)</option>
205
+ <option value="51">Pearl rush millet</option>
206
+ <option value="54">Finger millet</option>
207
+ <option value="56">White sorghum</option>
208
+ <option value="57">Red irish potato</option>
209
+ <option value="58">Cabbages</option>
210
+ <option value="59">Sweet potatoes</option>
211
+ <option value="60">Carrots</option>
212
+ <option value="61">Tomatoes</option>
213
+ <option value="64">Beans rosecoco</option>
214
+ <option value="65">Beans (mwitemania)</option>
215
+ <option value="66">Beans (mwezi moja)</option>
216
+ <option value="67">Beans (canadian wonder)</option>
217
+ <option value="68">Tilapia</option>
218
+ <option value="70">Goat milk (at collection point)</option>
219
+ <option value="72">Eggs</option>
220
+ <option value="73">Meat beef</option>
221
+ <option value="74">Meat mutton</option>
222
+ <option value="75">Meat indiginous chicken</option>
223
+ <option value="76">Meat broiler</option>
224
+ <option value="77">Omena</option>
225
+ <option value="78">Cat fish(mkizi/fume)</option>
226
+ <option value="79">Haplochromis</option>
227
+ <option value="80">Trout</option>
228
+ <option value="81">Common carp</option>
229
+ <option value="82">Protopterus</option>
230
+ <option value="83">Black bass</option>
231
+ <option value="84">Labeo</option>
232
+ <option value="85">Mormyrus</option>
233
+ <option value="86">Eel </option>
234
+ <option value="87">African butter catfish</option>
235
+ <option value="89">Synodontis</option>
236
+ <option value="90">Alestes</option>
237
+ <option value="91">Barbus</option>
238
+ <option value="93">Snappers(tazanda)</option>
239
+ <option value="94">Rabbitfish (tafi/tasi)</option>
240
+ <option value="95">Mixed demersal</option>
241
+ <option value="96">Barracuda(kasumba)</option>
242
+ <option value="97">Tuna</option>
243
+ <option value="98">Mackerel</option>
244
+ <option value="99">Kingfish (nguru)</option>
245
+ <option value="100">Sharks</option>
246
+ <option value="101">Sardines</option>
247
+ <option value="102">Swordfishes</option>
248
+ <option value="103">Lobster(kamba mawe)</option>
249
+ <option value="104">Prawns</option>
250
+ <option value="107">Mud crabs</option>
251
+ <option value="108">Golden (deep-sea) crabs kaa</option>
252
+ <option value="109">Fresh water shrimp</option>
253
+ <option value="110">Octopus (pweza)</option>
254
+ <option value="111">Cuttlefish</option>
255
+ <option value="112">Squid(ngisi)</option>
256
+ <option value="113">Oysters</option>
257
+ <option value="114">Fish oil</option>
258
+ <option value="115">Fish maws</option>
259
+ <option value="117">Nile perch skins</option>
260
+ <option value="121">Black nightshade (managu/ osuga)</option>
261
+ <option value="122">Spider flower (saga)</option>
262
+ <option value="123">Amaranthus (terere)</option>
263
+ <option value="124">Jute plant (murenda)</option>
264
+ <option value="125">Passion fruits</option>
265
+ <option value="127">Oranges</option>
266
+ <option value="128">Tree tomato</option>
267
+ <option value="129">Pepino melon</option>
268
+ <option value="130">Thorn melon</option>
269
+ <option value="131">Yam</option>
270
+ <option value="133">Cow milk(at collection point)</option>
271
+ <option value="134">Camel milk(at collection point)</option>
272
+ <option value="138">Camel milk(processed)</option>
273
+ <option value="139">Goat milk (processed)</option>
274
+ <option value="140">Cattle</option>
275
+ <option value="141">Pork</option>
276
+ <option value="142">Avocado</option>
277
+ <option value="143">Arrow root</option>
278
+ <option value="145">Lemons</option>
279
+ <option value="147">Mangoes</option>
280
+ <option value="148">Limes</option>
281
+ <option value="149">Green maize</option>
282
+ <option value="150">Water melon</option>
283
+ <option value="151">Pineapples</option>
284
+ <option value="152">Pawpaw</option>
285
+ <option value="153">Cow milk(processd)</option>
286
+ <option value="154">Kales/sukuma wiki</option>
287
+ <option value="158">Dry onions</option>
288
+ <option value="159">Spring onions</option>
289
+ <option value="160">Fresh peas</option>
290
+ <option value="161">Spinach</option>
291
+ <option value="162">Cassava fresh</option>
292
+ <option value="163">White irish potatoes</option>
293
+ <option value="164">Cassava chips (dry)</option>
294
+ <option value="165">Chillies</option>
295
+ <option value="166">Lettuce</option>
296
+ <option value="167">Sheep</option>
297
+ <option value="168">Goat</option>
298
+ <option value="169">Donkey</option>
299
+ <option value="170">Pumpkin</option>
300
+ <option value="171">Butternuts</option>
301
+ <option value="172">Capsicums</option>
302
+ <option value="173">Cucumber</option>
303
+ <option value="174">Egg plant (brinjals)</option>
304
+ <option value="175">Cauliflower</option>
305
+ <option value="177">French beans</option>
306
+ <option value="178">Ginger</option>
307
+ <option value="180">Garlic</option>
308
+ <option value="182">Njugu mawe</option>
309
+ <option value="183">Beans rosecoco (nyayo)</option>
310
+ <option value="184">Nile perch</option>
311
+ <option value="186">Camel</option>
312
+ <option value="187">Rabbit</option>
313
+ <option value="188">Pigeon peas</option>
314
+ <option value="189">Cowpeas</option>
315
+ <option value="190">Scavengers (changu/tangu)</option>
316
+ <option value="191">Parrotfishes(pono)</option>
317
+ <option value="192">Groupers</option>
318
+ <option value="193">Grunt(taamamba/kora)</option>
319
+ <option value="194">Mullets(fumi)</option>
320
+ <option value="195">Surgeonfishes</option>
321
+ <option value="196">Threadfin breams</option>
322
+ <option value="197">Goatfishes</option>
323
+ <option value="198">Rayfish</option>
324
+ <option value="199">Needlefishes</option>
325
+ <option value="200">Jacks/trevallies(kolekole)</option>
326
+ <option value="201">Halfbeaks</option>
327
+ <option value="202">Anchovies</option>
328
+ <option value="203">Sailfishes</option>
329
+ <option value="204">Wolf herrings</option>
330
+ <option value="205">Marlins</option>
331
+ <option value="206">Jobfish</option>
332
+ <option value="207">Mixed pelagics</option>
333
+ <option value="208">Camel meat</option>
334
+ <option value="209">Meat chevon</option>
335
+ <option value="210">Rabbit meat </option>
336
+ <option value="211">Pigs</option>
337
+ <option value="212">Honey</option>
338
+ <option value="213">Cattle hide</option>
339
+ <option value="214">Camel hide</option>
340
+ <option value="215">Goat skin</option>
341
+ <option value="216">Sheep skin</option>
342
+ <option value="217">Fertilizer</option>
343
+ <option value="218">Tea</option>
344
+ <option value="219">Coffee</option>
345
+ <option value="220">Wheat bran</option>
346
+ <option value="221">Maize bran</option>
347
+ <option value="222">Sunflower cake</option>
348
+ <option value="223">Cotton seed</option>
349
+ <option value="224">Cotton</option>
350
+ <option value="226">Banana (ripening)</option>
351
+ <option value="227">Chicken</option>
352
+ <option value="228">Macadamia seed</option>
353
+ <option value="229">Cashewnuts (korosho)</option>
354
+ <option value="230">Cowpea leaves (kunde)</option>
355
+ <option value="231">Nderema- vine spinach</option>
356
+ <option value="233">Pumpkin leaves</option>
357
+ <option value="234">Ethiopian kales -kanzira</option>
358
+ <option value="235">Indigenous crotolaria (mito/miro)</option>
359
+ <option value="237">Soybean oil</option>
360
+ <option value="238">Coconut oil</option>
361
+ <option value="239">Sunflower seeds</option>
362
+ <option value="240">Sunflower oil</option>
363
+ <option value="241">Walnut seed</option>
364
+ <option value="242">Coriander (dhania)</option>
365
+ <option value="243">Grapes</option>
366
+ <option value="244">Apples</option>
367
+ <option value="245">Dry peas</option>
368
+ <option value="246">Mixed beans</option>
369
+ <option value="247">Rockcode(tewa)</option>
370
+ <option value="248">Queenfish (pandu)</option>
371
+ <option value="249">Maize flour</option>
372
+ <option value="251">Duck</option>
373
+ <option value="255">Banana (cooking)</option>
374
+ <option value="256">Banana (plantain)</option>
375
+ <option value="257">Courgette</option>
376
+ <option value="258">Broccoli</option>
377
+ <option value="259">Lentils</option>
378
+ <option value="261">Fish scales</option>
379
+ <option value="262">Tangerine (sandara)</option>
380
+ <option value="265">Wheat flour</option>
381
+ <option value="267">Paddy rice</option>
382
+ <option value="269">Beans (yellow)</option>
383
+ <option value="270">Coconut</option>
384
+ <option value="272">Okra (lady's fingers or gumbo)</option>
385
+ <option value="273">Dragon fruit</option>
386
+ </select>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ <div class="row">
391
+ <div class="col-md-4">
392
+ <div class="form-group">
393
+ <label for="inputStart">Start Date</label>
394
+ <input type="text" name="start" class="form-control form-date" id="dateStartSearch" value="2024-01-15">
395
+ </div>
396
+ </div>
397
+ <div class="col-md-4">
398
+ <div class="form-group">
399
+ <label for="inputEnd">End Date</label>
400
+ <input type="text" name="end" class="form-control form-date" id="dateEndSearch" value="2024-01-20">
401
+ </div>
402
+ </div>
403
+ <div class="col-md-4">
404
+ <div class="form-group">
405
+ <label>Entries:</label>
406
+ <select id="selPerPage" name="per_page" class="page-size form-control" onchange='this.form.submit()'>
407
+ <option value="10">10</option>
408
+ <option value="50">50</option>
409
+ <option value="100">100</option>
410
+ <option value="500">500</option>
411
+ <option value="1500">1500</option>
412
+ <option value="3000">3000</option>
413
+ </select>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ <div class="row">
418
+ <div class="col-md-4 offset-md-8">
419
+ <div class="form-group">
420
+ <input type="submit" value="Filter Prices" class="btn btn-primary w-100 mt-3">
421
+ </div>
422
+ </div>
423
+ </div>
424
+ </form>
425
+ </div>
426
+ </div>
427
+ </div>
428
+ </div>
429
+ </div>
430
+ <div class="col-lg-12 mt-3">
431
+ <div class="panel panel-default">
432
+ <div class="panel-body">
433
+ <div class="card bg-light">
434
+ <div class="card-body">
435
+ <div class="row d-flex align-items-center justify-content-between mt-1 mb-3">
436
+ <div class="pull-left col-lg-6">
437
+ <ul class="pagination"><li class="active"><a href="#">1</a></li><li><a href="https://kamis.kilimo.go.ke/site/market_search/5?product%5B0%5D=1&county%5B0%5D=Nakuru&start=2024-01-15&end=2024-01-20&per_page=5" data-ci-pagination-page="2">2</a></li><li><a href="https://kamis.kilimo.go.ke/site/market_search/10?product%5B0%5D=1&county%5B0%5D=Nakuru&start=2024-01-15&end=2024-01-20&per_page=5" data-ci-pagination-page="3">3</a></li><li><a href="https://kamis.kilimo.go.ke/site/market_search/15?product%5B0%5D=1&county%5B0%5D=Nakuru&start=2024-01-15&end=2024-01-20&per_page=5" data-ci-pagination-page="4">4</a></li><li><a href="https://kamis.kilimo.go.ke/site/market_search/5?product%5B0%5D=1&county%5B0%5D=Nakuru&start=2024-01-15&end=2024-01-20&per_page=5" data-ci-pagination-page="2" rel="next">&gt;</a><li></ul> </div>
438
+ <div class="pull-right col-lg-2">
439
+ <a href="https://kamis.kilimo.go.ke/site/market_search?product%5B%5D=1&county%5B%5D=Nakuru&start=2024-01-15&end=2024-01-20&per_page=5&export=excel" class="btn btn-default mr-2 mt-2 mb-2 btn-export"><i class="fas fa-file-excel"></i> Export to Excel</a> </div>
440
+ </div>
441
+ <div class="row">
442
+ <div class="col-md-12">
443
+ <div class="table-responsive">
444
+ <table class="table table-bordered table-condensed">
445
+ <thead>
446
+ <tr>
447
+ <th>Market</th><th>Commodity</th><th>Classification</th><th>Grade</th><th>Sex</th><th>Wholesale</th><th>Retail</th><th>Supply Volume</th><th>County</th><th>Date</th></tr>
448
+ </thead>
449
+ <tbody>
450
+ <tr>
451
+ <td>Molo</td><td>Dry Maize</td><td>White Maize</td><td>-</td><td>-</td><td> - </td><td>40.00/Kg</td><td>2100.00</td><td>Nakuru</td><td>2024-01-20</td></tr>
452
+ <tr>
453
+ <td>Molo</td><td>Dry Maize</td><td>White Maize</td><td>-</td><td>-</td><td> - </td><td>3600.00/Kg</td><td>2400.00</td><td>Nakuru</td><td>2024-01-19</td></tr>
454
+ <tr>
455
+ <td>Nakuru Wakulima</td><td>Dry Maize</td><td>Yellow Maize</td><td>-</td><td>-</td><td>70.00/Kg</td><td> - </td><td></td><td>Nakuru</td><td>2024-01-19</td></tr>
456
+ <tr>
457
+ <td>Nakuru Wakulima</td><td>Dry Maize</td><td>White Maize</td><td>-</td><td>-</td><td>46.67/Kg</td><td> - </td><td>4590.00</td><td>Nakuru</td><td>2024-01-19</td></tr>
458
+ <tr>
459
+ <td>Molo</td><td>Dry Maize</td><td>White Maize</td><td>-</td><td>-</td><td> - </td><td>151.11/Kg</td><td>2400.00</td><td>Nakuru</td><td>2024-01-19</td></tr>
460
+ </tbody>
461
+ </table> </div>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ <script type="text/javascript">
470
+ $(document).ready(function() {
471
+ $('#selPerPage').select2({placeholder : "Select Page"});
472
+ if(("5")){
473
+ $('#selPerPage').select2('destroy');
474
+ $('#selPerPage').val("5").select2();
475
+ }
476
+ $('#selProduct').select2({
477
+ placeholder : "Select Product",
478
+ });
479
+ if(["1"]){
480
+ $('#selProduct').val(["1"])
481
+ $('#selProduct').trigger('change');
482
+ }
483
+
484
+ $('#selMarket').select2({
485
+ placeholder : "Select Market",
486
+ });
487
+
488
+ $('#selCounty').select2({
489
+ placeholder : "Select County",
490
+ });
491
+ if(["Nakuru"]){
492
+ $('#selCounty').val(["Nakuru"])
493
+ $('#selCounty').trigger('change');
494
+ fetchMarkets();
495
+ }
496
+
497
+ $('#dateStartSearch').Zebra_DatePicker({
498
+ show_icon: false,
499
+ });
500
+ if("2024-01-15"){
501
+ $("#dateStartSearch").val("2024-01-15");
502
+ }
503
+ $('#dateEndSearch').Zebra_DatePicker({
504
+ show_icon: false
505
+ });
506
+ if("2024-01-20"){
507
+ $("#dateEndSearch").val("2024-01-20");
508
+ }
509
+ $('#selCounty').change(function(){
510
+ fetchMarkets();
511
+ });
512
+ });
513
+ function fetchMarkets(){
514
+ let select_value = $('#selCounty').val();
515
+ axios.get("https:\/\/kamis.kilimo.go.ke\/"+'index.php/site/county_markets',{
516
+ params: {
517
+ counties: select_value
518
+ }
519
+ }).then((response) => {
520
+ var items = [];
521
+ $('#selMarket').val(null).trigger('change');
522
+ $('#selMarket').find('option').remove().end();
523
+ console.log(response);
524
+ response.data.forEach((j) => {
525
+ if(null){
526
+ $("#selMarket").append(new Option( j.trade_point_name,j.id, false, false));
527
+ $('#selMarket').val(null);
528
+ $('#selMarket').trigger('change');
529
+ return;
530
+ }
531
+ $("#selMarket").append(new Option( j.trade_point_name,j.id, false, false)).trigger('change');
532
+ });
533
+ });
534
+ }
535
+ // </script>
536
+ </div>
537
+ </section>
538
+ <a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i
539
+ class="bi bi-arrow-up-short"></i></a>
540
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/bootstrap/js/bootstrap.bundle.js"></script>
541
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/aos/aos.js"></script>
542
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/php-email-form/validate.js"></script>
543
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/swiper/swiper-bundle.min.js"></script>
544
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/purecounter/purecounter.js"></script>
545
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/isotope-layout/isotope.pkgd.min.js"></script>
546
+ <script src="https://kamis.kilimo.go.ke/resources/frontpage-vendor/glightbox/js/glightbox.min.js"></script>
547
+ <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
548
+ <script src="https://kamis.kilimo.go.ke/resources/js/frontpage/main.js"></script>
549
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/fake_data.js"></script>-->
550
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/agricultural-inputs/table.js"></script>-->
551
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/agricultural-inputs/graph.js"></script>-->
552
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/agricultural-inputs/whats-new.js"></script>-->
553
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/crops/table.js"></script>-->
554
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/crops/graph.js"></script>-->
555
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/crops/whats-new.js"></script>-->
556
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/fisheries/table.js"></script>-->
557
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/fisheries/graph.js"></script>-->
558
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/fisheries/whats-new.js"></script>-->
559
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/livestock/table.js"></script>-->
560
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/livestock/graph.js"></script>-->
561
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/livestock/whats-new.js"></script>-->
562
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/agricultural/table.js"></script>-->
563
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/agricultural/graph.js"></script>-->
564
+ <!--<script src="https://kamis.kilimo.go.ke/resources/js/frontpage/agricultural/whats-new.js"></script>-->
565
+ </body>
566
+ </html>
tests/test_dpi.py CHANGED
@@ -3,10 +3,23 @@
3
  Covers the Aadhaar + Land Record + KCC services, the deterministic
4
  registry, DPIAgent composite, and the KCC-aware credit readiness
5
  integration. No DB, no API key, no network.
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
9
 
 
 
 
 
 
 
 
 
 
10
 
11
  # ---------------------------------------------------------------------------
12
  # Module imports
 
3
  Covers the Aadhaar + Land Record + KCC services, the deterministic
4
  registry, DPIAgent composite, and the KCC-aware credit readiness
5
  integration. No DB, no API key, no network.
6
+
7
+ Kenya has no equivalent DPI stack (Aadhaar/KCC are India-specific) so
8
+ this whole module is India-only. Phase 1.6 will introduce parallel
9
+ Kenya DPI tests if/when a Kenya DPI subsystem exists.
10
  """
11
 
12
  from __future__ import annotations
13
 
14
+ import pytest
15
+
16
+ from config import REGION
17
+
18
+ pytestmark = pytest.mark.skipif(
19
+ REGION != "india",
20
+ reason="DPI subsystem is India-specific (Aadhaar/KCC); no Kenya equivalent yet",
21
+ )
22
+
23
 
24
  # ---------------------------------------------------------------------------
25
  # Module imports
tests/test_kamis_ingestion.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smoke tests for the KAMIS ingestion source.
2
+
3
+ Runs entirely in demo mode (no network) against the committed snapshot
4
+ at ``data/demo_snapshots/kamis/kamis_demo_snapshot.csv`` plus a static
5
+ HTML fixture copied from the lastmile-bench benchmark repo.
6
+
7
+ These tests deliberately construct their own lightweight market and
8
+ commodity objects rather than importing ``config.MANDIS`` — the
9
+ Kenya-migration ``config.py`` changes land in a parallel Phase 1.1
10
+ task, and we don't want this test's stability to depend on that
11
+ landing first.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import os
18
+ from dataclasses import dataclass
19
+ from datetime import date, datetime
20
+ from pathlib import Path
21
+
22
+ import pytest
23
+
24
+ from src.ingestion.kamis import (
25
+ PRODUCT_IDS,
26
+ build_search_url,
27
+ fetch_kamis_prices,
28
+ parse_search_result_html,
29
+ )
30
+
31
+
32
+ # --- Minimal Kenya-shaped market stub -----------------------------------
33
+ # Kept inline so the test doesn't couple to whatever Phase 1.1 picks for
34
+ # the canonical Kenya market dataclass. The ingestion module only reads
35
+ # `mandi_id`, `name`, and `district` / `county`, so this stub suffices.
36
+ @dataclass
37
+ class _KenyaMarket:
38
+ mandi_id: str
39
+ name: str
40
+ district: str # KAMIS county
41
+
42
+
43
+ # Three KAMIS markets that actually appear in the demo snapshot.
44
+ _DEMO_MARKETS = [
45
+ _KenyaMarket(mandi_id="KE-NAI-KAW", name="Kawangware", district="Nairobi"),
46
+ _KenyaMarket(mandi_id="KE-NAI-KAN", name="Kangemi Market", district="Nairobi"),
47
+ _KenyaMarket(mandi_id="KE-NAK-MOL", name="Molo", district="Nakuru"),
48
+ ]
49
+
50
+ # The four Kenya priority crops for the market-intelligence pivot.
51
+ _DEMO_COMMODITIES: list[dict] = [
52
+ {"id": "dry_maize", "name": "Dry maize", "kamis_name": "Dry maize"},
53
+ {"id": "beans", "name": "Beans", "kamis_name": "Beans (yellow-green)"},
54
+ {"id": "red_irish_potato", "name": "Red irish potato",
55
+ "kamis_name": "Red irish potato"},
56
+ {"id": "green_grams", "name": "Green grams", "kamis_name": "Green grams"},
57
+ ]
58
+
59
+
60
+ # --- Tests --------------------------------------------------------------
61
+
62
+
63
+ def test_demo_mode_returns_valid_price_records(monkeypatch: pytest.MonkeyPatch) -> None:
64
+ """End-to-end demo-mode smoke test.
65
+
66
+ Drives `fetch_kamis_prices` against the committed snapshot and
67
+ asserts every returned record has the schema the downstream
68
+ reconciliation / forecasting / optimization steps expect.
69
+ """
70
+ monkeypatch.setenv("MARKET_INTEL_DEMO_MODE", "1")
71
+
72
+ result = asyncio.run(fetch_kamis_prices(
73
+ markets=_DEMO_MARKETS,
74
+ commodities=_DEMO_COMMODITIES,
75
+ days_back=60,
76
+ ))
77
+
78
+ # Keys are mandi IDs, at least one market has data.
79
+ assert set(result.keys()) == {m.mandi_id for m in _DEMO_MARKETS}
80
+ all_records = [r for recs in result.values() for r in recs]
81
+ assert len(all_records) > 0, "demo snapshot returned zero records"
82
+
83
+ # Every record has the schema the pipeline downstream depends on.
84
+ valid_commodity_ids = {c["id"] for c in _DEMO_COMMODITIES}
85
+ valid_mandi_ids = {m.mandi_id for m in _DEMO_MARKETS}
86
+ for rec in all_records:
87
+ assert rec.source == "kamis"
88
+ assert rec.mandi_id in valid_mandi_ids, f"unknown mandi_id: {rec.mandi_id}"
89
+ assert rec.commodity_id in valid_commodity_ids, (
90
+ f"unknown commodity_id: {rec.commodity_id}"
91
+ )
92
+ # ISO date string
93
+ parsed = datetime.strptime(rec.date, "%Y-%m-%d").date()
94
+ assert parsed <= date.today() + timedelta_days(1), "date in far future"
95
+ # Positive prices
96
+ assert rec.modal_price_rs > 0, f"non-positive modal: {rec}"
97
+ assert rec.min_price_rs > 0, f"non-positive min: {rec}"
98
+ assert rec.max_price_rs > 0, f"non-positive max: {rec}"
99
+ # Quality flag is one of the documented values
100
+ assert rec.quality_flag in {"good", "stale", "anomalous", "missing"}
101
+
102
+
103
+ def test_demo_mode_covers_at_least_two_commodities(monkeypatch: pytest.MonkeyPatch) -> None:
104
+ """Guard against a regression where the snapshot or filter loses variety."""
105
+ monkeypatch.setenv("MARKET_INTEL_DEMO_MODE", "1")
106
+ result = asyncio.run(fetch_kamis_prices(
107
+ markets=_DEMO_MARKETS,
108
+ commodities=_DEMO_COMMODITIES,
109
+ days_back=60,
110
+ ))
111
+ observed = {r.commodity_id for recs in result.values() for r in recs}
112
+ assert len(observed) >= 2, (
113
+ f"expected >=2 commodities in demo snapshot, got {observed}"
114
+ )
115
+
116
+
117
+ def test_build_search_url_uses_iso_dates() -> None:
118
+ """Hard-guard against the localized-date footgun documented in the
119
+ benchmark reference (localized dates fail silently with "No Data")."""
120
+ url = build_search_url(
121
+ product_id=1,
122
+ county="Nairobi",
123
+ start=date(2026, 1, 1),
124
+ end=date(2026, 4, 1),
125
+ )
126
+ assert "start=2026-01-01" in url
127
+ assert "end=2026-04-01" in url
128
+ assert "product%5B%5D=1" in url # product[]=1 URL-encoded
129
+ assert "county%5B%5D=Nairobi" in url
130
+
131
+
132
+ def test_product_ids_covers_four_kenya_priority_crops() -> None:
133
+ """Pin the four v0.1 Kenya crops — if any of these IDs change upstream
134
+ the test fails fast instead of silently returning zero rows."""
135
+ assert PRODUCT_IDS["Dry maize"] == 1
136
+ assert PRODUCT_IDS["Beans (yellow-green)"] == 30
137
+ assert PRODUCT_IDS["Red irish potato"] == 57
138
+ assert PRODUCT_IDS["Green grams"] == 10
139
+
140
+
141
+ # --- HTML parser test (fixture-based) -----------------------------------
142
+
143
+
144
+ _FIXTURE_PATH = (
145
+ Path(__file__).parent / "fixtures" / "kamis_nakuru_dry_maize_sample.html"
146
+ )
147
+
148
+
149
+ @pytest.mark.skipif(
150
+ not _FIXTURE_PATH.exists(),
151
+ reason="KAMIS HTML fixture not available; copy from lastmile-bench",
152
+ )
153
+ def test_parse_search_result_html_on_nakuru_fixture() -> None:
154
+ """Parse the committed HTML fixture and assert the core row shape.
155
+
156
+ Using the fixture rather than live fetch keeps the test hermetic.
157
+ """
158
+ html = _FIXTURE_PATH.read_text(encoding="utf-8")
159
+ rows = parse_search_result_html(html)
160
+ assert len(rows) > 0, "fixture parsed to zero rows — parser regression"
161
+ for row in rows:
162
+ assert row["market"], "empty market cell"
163
+ assert row["product"], "empty product cell"
164
+ assert row["wholesale"] is not None and row["wholesale"] > 0
165
+ assert row["retail"] is not None and row["retail"] > 0
166
+ assert isinstance(row["price_date"], date)
167
+
168
+
169
+ # --- tiny helper --------------------------------------------------------
170
+
171
+
172
+ def timedelta_days(n: int): # tiny local shim so we don't add another import
173
+ from datetime import timedelta
174
+ return timedelta(days=n)
tests/test_smoke.py CHANGED
@@ -18,6 +18,13 @@ from __future__ import annotations
18
 
19
  import pytest
20
 
 
 
 
 
 
 
 
21
 
22
  # ---------------------------------------------------------------------------
23
  # Module imports
@@ -58,6 +65,7 @@ def test_imports_optimizer():
58
  # ---------------------------------------------------------------------------
59
 
60
 
 
61
  def test_mandis_and_commodities_loaded():
62
  """15 Tamil Nadu mandis and 10 commodities are registered."""
63
  from config import COMMODITIES, MANDIS, MANDI_MAP
@@ -75,6 +83,7 @@ def test_pilot_registry_has_100_farmers():
75
  assert len(SAMPLE_FARMERS) == 100
76
 
77
 
 
78
  def test_featured_subset_is_three_curated_farmers():
79
  """FEATURED_FARMERS is exactly the 3 hand-written personas."""
80
  from config import FEATURED_FARMERS, SAMPLE_FARMERS
@@ -108,6 +117,7 @@ def test_generated_farmers_use_mandi_commodities():
108
  )
109
 
110
 
 
111
  def test_generated_farmers_have_plausible_quantities():
112
  """Generated quantities stay in the 8-35 quintal pilot range."""
113
  from config import SAMPLE_FARMERS
 
18
 
19
  import pytest
20
 
21
+ from config import REGION
22
+
23
+ _india_only = pytest.mark.skipif(
24
+ REGION != "india",
25
+ reason="India-specific assertions (15 mandis, Tamil names, quintal range); Kenya parity tests in Phase 1.6",
26
+ )
27
+
28
 
29
  # ---------------------------------------------------------------------------
30
  # Module imports
 
65
  # ---------------------------------------------------------------------------
66
 
67
 
68
+ @_india_only
69
  def test_mandis_and_commodities_loaded():
70
  """15 Tamil Nadu mandis and 10 commodities are registered."""
71
  from config import COMMODITIES, MANDIS, MANDI_MAP
 
83
  assert len(SAMPLE_FARMERS) == 100
84
 
85
 
86
+ @_india_only
87
  def test_featured_subset_is_three_curated_farmers():
88
  """FEATURED_FARMERS is exactly the 3 hand-written personas."""
89
  from config import FEATURED_FARMERS, SAMPLE_FARMERS
 
117
  )
118
 
119
 
120
+ @_india_only
121
  def test_generated_farmers_have_plausible_quantities():
122
  """Generated quantities stay in the 8-35 quintal pilot range."""
123
  from config import SAMPLE_FARMERS