nivakaran commited on
Commit
2473009
·
verified ·
1 Parent(s): 4dcfed0

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -525,8 +525,8 @@ python main.py --mode train --epochs 100
525
  ```
526
  ┌─────────────────────────────────────────────────┐
527
  │ MultiCollectionRetriever │
528
- │ - Connects to ALL ChromaDB collections
529
- │ - Roger_feeds, Roger_rag_collection, etc.
530
  └─────────────────┬───────────────────────────────┘
531
 
532
 
 
525
  ```
526
  ┌─────────────────────────────────────────────────┐
527
  │ MultiCollectionRetriever │
528
+ │ - Connects to ChromaDB intelligence collection
529
+ │ - Roger_feeds (all agent domain feeds)
530
  └─────────────────┬───────────────────────────────┘
531
 
532
 
frontend/app/components/FloatingChatBox.tsx CHANGED
@@ -169,7 +169,7 @@ const FloatingChatBox = () => {
169
  </div>
170
 
171
  {/* Domain Filter - scrollable on mobile */}
172
- <div className="flex gap-1.5 sm:gap-1 px-3 sm:px-4 py-3 bg-[#1a1a1a] border-b border-[#373435] overflow-x-auto sm:flex-wrap hide-scrollbar">
173
  <Badge
174
  className={`cursor-pointer text-xs sm:text-xs whitespace-nowrap px-3 py-1.5 sm:px-2 sm:py-1 transition-colors touch-manipulation ${!domainFilter ? 'bg-green-500 text-white' : 'bg-[#373435] text-gray-300 hover:bg-[#4a4a4a] active:bg-[#555]'}`}
175
  onClick={() => setDomainFilter(null)}
 
169
  </div>
170
 
171
  {/* Domain Filter - scrollable on mobile */}
172
+ <div className="flex gap-1.5 sm:gap-1 px-3 sm:px-4 py-3 bg-[#1a1a1a] border-b border-[#373435] overflow-x-auto sm:flex-wrap intel-scrollbar">
173
  <Badge
174
  className={`cursor-pointer text-xs sm:text-xs whitespace-nowrap px-3 py-1.5 sm:px-2 sm:py-1 transition-colors touch-manipulation ${!domainFilter ? 'bg-green-500 text-white' : 'bg-[#373435] text-gray-300 hover:bg-[#4a4a4a] active:bg-[#555]'}`}
175
  onClick={() => setDomainFilter(null)}
frontend/app/components/dashboard/AnomalyDetection.tsx CHANGED
@@ -132,7 +132,7 @@ const AnomalyDetection = () => {
132
  <Separator className="mb-4" />
133
 
134
  {/* Anomalies List */}
135
- <div className="space-y-3 max-h-[500px] overflow-y-auto">
136
  {loading && anomalies.length === 0 ? (
137
  <div className="text-center py-8">
138
  <RefreshCw className="w-8 h-8 mx-auto animate-spin text-primary mb-3" />
 
132
  <Separator className="mb-4" />
133
 
134
  {/* Anomalies List */}
135
+ <div className="space-y-3 max-h-[500px] overflow-y-auto intel-scrollbar pr-2">
136
  {loading && anomalies.length === 0 ? (
137
  <div className="text-center py-8">
138
  <RefreshCw className="w-8 h-8 mx-auto animate-spin text-primary mb-3" />
frontend/app/components/dashboard/CommodityPrices.tsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { ShoppingBasket, TrendingUp, TrendingDown, Minus } from "lucide-react";
6
+
7
+ interface Commodity {
8
+ name: string;
9
+ price: number;
10
+ unit: string;
11
+ change: number;
12
+ category: string;
13
+ }
14
+
15
+ interface CommodityPricesProps {
16
+ commodityData?: Record<string, unknown> | null;
17
+ }
18
+
19
+ const CommodityPrices = ({ commodityData }: CommodityPricesProps) => {
20
+ const commodities = (commodityData?.commodities as Commodity[]) || [];
21
+ const summary = (commodityData?.summary as Record<string, number>) || {};
22
+ const fetchedAt = commodityData?.fetched_at as string;
23
+
24
+ // Show top 8 essential items
25
+ const essentialItems = commodities.slice(0, 8);
26
+
27
+ const getTrendIcon = (change: number) => {
28
+ if (change > 0) return <TrendingUp className="w-3 h-3 text-destructive" />;
29
+ if (change < 0) return <TrendingDown className="w-3 h-3 text-success" />;
30
+ return <Minus className="w-3 h-3 text-muted-foreground" />;
31
+ };
32
+
33
+ const getChangeColor = (change: number) => {
34
+ if (change > 0) return "text-destructive";
35
+ if (change < 0) return "text-success";
36
+ return "text-muted-foreground";
37
+ };
38
+
39
+ return (
40
+ <Card className="p-4 bg-card border-border">
41
+ <div className="flex items-center justify-between mb-3">
42
+ <div className="flex items-center gap-2">
43
+ <div className="p-2 rounded-lg bg-green-500/20">
44
+ <ShoppingBasket className="w-5 h-5 text-green-500" />
45
+ </div>
46
+ <div>
47
+ <h3 className="font-bold text-sm">🛒 COMMODITIES</h3>
48
+ <p className="text-xs text-muted-foreground">Essential goods prices</p>
49
+ </div>
50
+ </div>
51
+ <div className="flex gap-1">
52
+ {summary.items_increased > 0 && (
53
+ <Badge className="bg-destructive/20 text-destructive text-xs">
54
+ ↑{summary.items_increased}
55
+ </Badge>
56
+ )}
57
+ {summary.items_decreased > 0 && (
58
+ <Badge className="bg-success/20 text-success text-xs">
59
+ ↓{summary.items_decreased}
60
+ </Badge>
61
+ )}
62
+ </div>
63
+ </div>
64
+
65
+ <div className="grid grid-cols-2 gap-1.5">
66
+ {essentialItems.map((item, idx) => (
67
+ <div key={idx} className="p-2 rounded bg-muted/30 border border-border">
68
+ <div className="flex items-center justify-between">
69
+ <span className="text-xs text-muted-foreground truncate flex-1">{item.name}</span>
70
+ {getTrendIcon(item.change)}
71
+ </div>
72
+ <div className="flex items-baseline gap-1 mt-0.5">
73
+ <span className="text-sm font-bold">Rs.{item.price}</span>
74
+ {item.change !== 0 && (
75
+ <span className={`text-xs ${getChangeColor(item.change)}`}>
76
+ {item.change > 0 ? '+' : ''}{item.change}
77
+ </span>
78
+ )}
79
+ </div>
80
+ </div>
81
+ ))}
82
+ </div>
83
+
84
+ {fetchedAt && (
85
+ <p className="text-xs text-muted-foreground mt-3 text-center">
86
+ Source: Consumer Affairs Authority
87
+ </p>
88
+ )}
89
+ </Card>
90
+ );
91
+ };
92
+
93
+ export default CommodityPrices;
frontend/app/components/dashboard/CurrencyPrediction.tsx CHANGED
@@ -122,8 +122,8 @@ export default function CurrencyPrediction() {
122
  {/* Main Prediction Card */}
123
  <div
124
  className={`p-6 rounded-xl border mb-6 ${prediction.expected_change_pct < 0
125
- ? "bg-green-500/10 border-green-500/30"
126
- : "bg-red-500/10 border-red-500/30"
127
  }`}
128
  >
129
  <div className="grid grid-cols-3 gap-4 text-center">
@@ -153,8 +153,8 @@ export default function CurrencyPrediction() {
153
  <span className="text-slate-400">Expected Change: </span>
154
  <span
155
  className={`font-bold ${prediction.expected_change_pct < 0
156
- ? "text-green-400"
157
- : "text-red-400"
158
  }`}
159
  >
160
  {prediction.expected_change_pct > 0 ? "+" : ""}
@@ -223,12 +223,6 @@ export default function CurrencyPrediction() {
223
  </div>
224
  )}
225
 
226
- {/* Fallback Warning */}
227
- {prediction.is_fallback && (
228
- <div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-sm text-yellow-400">
229
- ⚠️ Using fallback model. Run training for accurate predictions.
230
- </div>
231
- )}
232
 
233
  {/* Footer */}
234
  <div className="mt-4 text-xs text-slate-500 text-center">
 
122
  {/* Main Prediction Card */}
123
  <div
124
  className={`p-6 rounded-xl border mb-6 ${prediction.expected_change_pct < 0
125
+ ? "bg-green-500/10 border-green-500/30"
126
+ : "bg-red-500/10 border-red-500/30"
127
  }`}
128
  >
129
  <div className="grid grid-cols-3 gap-4 text-center">
 
153
  <span className="text-slate-400">Expected Change: </span>
154
  <span
155
  className={`font-bold ${prediction.expected_change_pct < 0
156
+ ? "text-green-400"
157
+ : "text-red-400"
158
  }`}
159
  >
160
  {prediction.expected_change_pct > 0 ? "+" : ""}
 
223
  </div>
224
  )}
225
 
 
 
 
 
 
 
226
 
227
  {/* Footer */}
228
  <div className="mt-4 text-xs text-slate-500 text-center">
frontend/app/components/dashboard/DashboardOverview.tsx CHANGED
@@ -4,10 +4,28 @@ import { Badge } from "../ui/badge";
4
  import { useRogerData } from "../../hooks/use-roger-data";
5
  import { motion } from "framer-motion";
6
  import RiverNetStatus from "./RiverNetStatus";
 
 
 
 
 
 
7
 
8
  const DashboardOverview = () => {
9
- // Get riverData directly from hook (fetched via /api/rivernet)
10
- const { dashboard, events, isConnected, status, riverData } = useRogerData();
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  // Safety check: ensure events is always an array
13
  const safeEvents = events || [];
@@ -126,6 +144,18 @@ const DashboardOverview = () => {
126
  {/* RiverNet Flood Monitoring */}
127
  <RiverNetStatus riverData={riverData} compact={false} />
128
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  {/* Live Intelligence Feed - SORTED BY LATEST FIRST */}
130
  <Card className="p-6 bg-card border-border">
131
  <h3 className="font-bold mb-4 flex items-center gap-2">
@@ -134,7 +164,7 @@ const DashboardOverview = () => {
134
  <span className="text-xs text-muted-foreground ml-2">(Latest First)</span>
135
  <Badge className="ml-auto">{sortedEvents.length} Events</Badge>
136
  </h3>
137
- <div className="space-y-3 max-h-[500px] overflow-y-auto">
138
  {sortedEvents.slice(0, 10).map((event, idx) => {
139
  const isRisk = event.impact_type === 'risk';
140
  const isFlood = event.category === 'flood_monitoring' || event.category === 'flood_alert';
 
4
  import { useRogerData } from "../../hooks/use-roger-data";
5
  import { motion } from "framer-motion";
6
  import RiverNetStatus from "./RiverNetStatus";
7
+ import PowerOutageStatus from "./PowerOutageStatus";
8
+ import FuelPriceMonitor from "./FuelPriceMonitor";
9
+ import EconomicIndicators from "./EconomicIndicators";
10
+ import HealthAlerts from "./HealthAlerts";
11
+ import CommodityPrices from "./CommodityPrices";
12
+ import WaterSupplyStatus from "./WaterSupplyStatus";
13
 
14
  const DashboardOverview = () => {
15
+ // Get data from hook (fetched via various /api/ endpoints)
16
+ const {
17
+ dashboard,
18
+ events,
19
+ isConnected,
20
+ status,
21
+ riverData,
22
+ powerData,
23
+ fuelData,
24
+ economyData,
25
+ healthData,
26
+ commodityData,
27
+ waterData,
28
+ } = useRogerData();
29
 
30
  // Safety check: ensure events is always an array
31
  const safeEvents = events || [];
 
144
  {/* RiverNet Flood Monitoring */}
145
  <RiverNetStatus riverData={riverData} compact={false} />
146
 
147
+ {/* Situational Awareness Grid - NEW */}
148
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
149
+ <PowerOutageStatus powerData={powerData} />
150
+ <FuelPriceMonitor fuelData={fuelData} />
151
+ <EconomicIndicators economyData={economyData} />
152
+ <HealthAlerts healthData={healthData} />
153
+ <CommodityPrices commodityData={commodityData} />
154
+ <WaterSupplyStatus waterData={waterData} />
155
+ </div>
156
+
157
+
158
+
159
  {/* Live Intelligence Feed - SORTED BY LATEST FIRST */}
160
  <Card className="p-6 bg-card border-border">
161
  <h3 className="font-bold mb-4 flex items-center gap-2">
 
164
  <span className="text-xs text-muted-foreground ml-2">(Latest First)</span>
165
  <Badge className="ml-auto">{sortedEvents.length} Events</Badge>
166
  </h3>
167
+ <div className="space-y-3 max-h-[500px] overflow-y-auto intel-scrollbar pr-2">
168
  {sortedEvents.slice(0, 10).map((event, idx) => {
169
  const isRisk = event.impact_type === 'risk';
170
  const isFlood = event.category === 'flood_monitoring' || event.category === 'flood_alert';
frontend/app/components/dashboard/EconomicIndicators.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { TrendingUp, TrendingDown, Minus, Landmark, DollarSign, Percent, Building2 } from "lucide-react";
6
+
7
+ interface EconomicIndicatorsProps {
8
+ economyData?: Record<string, unknown> | null;
9
+ }
10
+
11
+ const EconomicIndicators = ({ economyData }: EconomicIndicatorsProps) => {
12
+ const indicators = (economyData?.indicators as Record<string, Record<string, unknown>>) || {};
13
+ const inflation = indicators?.inflation || {};
14
+ const policyRates = indicators?.policy_rates || {};
15
+ const exchangeRate = indicators?.exchange_rate || {};
16
+ const forexReserves = indicators?.forex_reserves || {};
17
+ const dataAsOf = economyData?.data_as_of as string;
18
+
19
+ const getTrendIcon = (trend: string) => {
20
+ if (trend === "improving" || trend === "stable") return <TrendingUp className="w-3 h-3 text-success" />;
21
+ if (trend === "declining") return <TrendingDown className="w-3 h-3 text-destructive" />;
22
+ return <Minus className="w-3 h-3 text-muted-foreground" />;
23
+ };
24
+
25
+ return (
26
+ <Card className="p-4 bg-card border-border">
27
+ <div className="flex items-center justify-between mb-3">
28
+ <div className="flex items-center gap-2">
29
+ <div className="p-2 rounded-lg bg-blue-500/20">
30
+ <Landmark className="w-5 h-5 text-blue-500" />
31
+ </div>
32
+ <div>
33
+ <h3 className="font-bold text-sm">🏛️ ECONOMY</h3>
34
+ <p className="text-xs text-muted-foreground">CBSL Indicators</p>
35
+ </div>
36
+ </div>
37
+ <Badge className="bg-muted text-muted-foreground">
38
+ {dataAsOf || "Latest"}
39
+ </Badge>
40
+ </div>
41
+
42
+ <div className="grid grid-cols-2 gap-2">
43
+ {/* Inflation */}
44
+ <div className="p-2 rounded-lg bg-muted/30 border border-border">
45
+ <div className="flex items-center gap-1 mb-1">
46
+ <Percent className="w-3 h-3 text-muted-foreground" />
47
+ <span className="text-xs text-muted-foreground">Inflation (YoY)</span>
48
+ </div>
49
+ <div className="flex items-center gap-1">
50
+ <span className="text-lg font-bold">{inflation.ccpi_yoy as number || 0}%</span>
51
+ {getTrendIcon(inflation.trend as string)}
52
+ </div>
53
+ </div>
54
+
55
+ {/* USD/LKR */}
56
+ <div className="p-2 rounded-lg bg-muted/30 border border-border">
57
+ <div className="flex items-center gap-1 mb-1">
58
+ <DollarSign className="w-3 h-3 text-muted-foreground" />
59
+ <span className="text-xs text-muted-foreground">USD/LKR</span>
60
+ </div>
61
+ <div className="flex items-center gap-1">
62
+ <span className="text-lg font-bold">{exchangeRate.usd_lkr as number || 0}</span>
63
+ {getTrendIcon(exchangeRate.trend as string)}
64
+ </div>
65
+ </div>
66
+
67
+ {/* Policy Rate */}
68
+ <div className="p-2 rounded-lg bg-muted/30 border border-border">
69
+ <div className="flex items-center gap-1 mb-1">
70
+ <Landmark className="w-3 h-3 text-muted-foreground" />
71
+ <span className="text-xs text-muted-foreground">SDFR Rate</span>
72
+ </div>
73
+ <span className="text-lg font-bold">{policyRates.sdfr as number || 0}%</span>
74
+ </div>
75
+
76
+ {/* Forex Reserves */}
77
+ <div className="p-2 rounded-lg bg-muted/30 border border-border">
78
+ <div className="flex items-center gap-1 mb-1">
79
+ <Building2 className="w-3 h-3 text-muted-foreground" />
80
+ <span className="text-xs text-muted-foreground">Reserves</span>
81
+ </div>
82
+ <div className="flex items-center gap-1">
83
+ <span className="text-lg font-bold">${forexReserves.value as number || 0}B</span>
84
+ {getTrendIcon(forexReserves.trend as string)}
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <p className="text-xs text-muted-foreground mt-3 text-center">
90
+ Source: Central Bank of Sri Lanka
91
+ </p>
92
+ </Card>
93
+ );
94
+ };
95
+
96
+ export default EconomicIndicators;
frontend/app/components/dashboard/FuelPriceMonitor.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Fuel, TrendingUp, TrendingDown, Minus } from "lucide-react";
6
+
7
+ interface FuelPrice {
8
+ price: number;
9
+ unit: string;
10
+ name: string;
11
+ }
12
+
13
+ interface FuelMonitorProps {
14
+ fuelData?: Record<string, unknown> | null;
15
+ }
16
+
17
+ const FuelPriceMonitor = ({ fuelData }: FuelMonitorProps) => {
18
+ const prices = (fuelData?.prices as Record<string, FuelPrice>) || {};
19
+ const lastRevision = fuelData?.last_revision as string;
20
+ const fetchedAt = fuelData?.fetched_at as string;
21
+
22
+ const fuelTypes = [
23
+ { key: "petrol_92", label: "Petrol 92", icon: "⛽" },
24
+ { key: "petrol_95", label: "Petrol 95", icon: "⛽" },
25
+ { key: "auto_diesel", label: "Diesel", icon: "🚛" },
26
+ { key: "kerosene", label: "Kerosene", icon: "🔥" },
27
+ ];
28
+
29
+ return (
30
+ <Card className="p-4 bg-card border-border">
31
+ <div className="flex items-center justify-between mb-3">
32
+ <div className="flex items-center gap-2">
33
+ <div className="p-2 rounded-lg bg-amber-500/20">
34
+ <Fuel className="w-5 h-5 text-amber-500" />
35
+ </div>
36
+ <div>
37
+ <h3 className="font-bold text-sm">⛽ FUEL PRICES</h3>
38
+ <p className="text-xs text-muted-foreground">CEYPETCO / LIOC</p>
39
+ </div>
40
+ </div>
41
+ <Badge className="bg-muted text-muted-foreground">
42
+ {lastRevision || "Latest"}
43
+ </Badge>
44
+ </div>
45
+
46
+ <div className="grid grid-cols-2 gap-2">
47
+ {fuelTypes.map(({ key, label, icon }) => {
48
+ const fuel = prices[key];
49
+ if (!fuel) return null;
50
+
51
+ return (
52
+ <div key={key} className="p-2 rounded-lg bg-muted/30 border border-border">
53
+ <div className="flex items-center justify-between mb-1">
54
+ <span className="text-xs text-muted-foreground">{icon} {label}</span>
55
+ </div>
56
+ <p className="text-lg font-bold text-foreground">
57
+ Rs. {fuel.price?.toFixed(0) || "-"}
58
+ </p>
59
+ <p className="text-xs text-muted-foreground">{fuel.unit || "LKR/L"}</p>
60
+ </div>
61
+ );
62
+ })}
63
+ </div>
64
+
65
+ {fetchedAt && (
66
+ <p className="text-xs text-muted-foreground mt-3 text-center">
67
+ Source: {fuelData?.source as string || "CEYPETCO"}
68
+ </p>
69
+ )}
70
+ </Card>
71
+ );
72
+ };
73
+
74
+ export default FuelPriceMonitor;
frontend/app/components/dashboard/HealthAlerts.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Heart, AlertTriangle, Bug, Activity } from "lucide-react";
6
+
7
+ interface HealthAlert {
8
+ type: string;
9
+ text: string;
10
+ severity: string;
11
+ }
12
+
13
+ interface HealthAlertsProps {
14
+ healthData?: Record<string, unknown> | null;
15
+ }
16
+
17
+ const HealthAlerts = ({ healthData }: HealthAlertsProps) => {
18
+ const alerts = (healthData?.alerts as HealthAlert[]) || [];
19
+ const dengue = (healthData?.dengue as Record<string, unknown>) || {};
20
+ const advisories = (healthData?.advisories as HealthAlert[]) || [];
21
+ const fetchedAt = healthData?.fetched_at as string;
22
+
23
+ const hasActiveAlerts = alerts.length > 0 || advisories.length > 0;
24
+
25
+ return (
26
+ <Card className="p-4 bg-card border-border">
27
+ <div className="flex items-center justify-between mb-3">
28
+ <div className="flex items-center gap-2">
29
+ <div className={`p-2 rounded-lg ${hasActiveAlerts ? 'bg-warning/20' : 'bg-success/20'}`}>
30
+ <Heart className={`w-5 h-5 ${hasActiveAlerts ? 'text-warning' : 'text-success'}`} />
31
+ </div>
32
+ <div>
33
+ <h3 className="font-bold text-sm">🏥 HEALTH STATUS</h3>
34
+ <p className="text-xs text-muted-foreground">Ministry of Health</p>
35
+ </div>
36
+ </div>
37
+ <Badge className={hasActiveAlerts ? "bg-warning/20 text-warning" : "bg-success/20 text-success"}>
38
+ {hasActiveAlerts ? "⚠ ADVISORIES" : "✓ NORMAL"}
39
+ </Badge>
40
+ </div>
41
+
42
+ {/* Dengue Section */}
43
+ <div className="p-3 rounded-lg bg-muted/30 border border-border mb-2">
44
+ <div className="flex items-center justify-between">
45
+ <div className="flex items-center gap-2">
46
+ <Bug className="w-4 h-4 text-warning" />
47
+ <span className="text-sm font-medium">Dengue Cases</span>
48
+ </div>
49
+ <div className="text-right">
50
+ <p className="text-lg font-bold">{dengue.weekly_cases as number || 0}</p>
51
+ <p className="text-xs text-muted-foreground">weekly avg</p>
52
+ </div>
53
+ </div>
54
+ {dengue.high_risk_districts && (
55
+ <div className="mt-2 flex flex-wrap gap-1">
56
+ {(dengue.high_risk_districts as string[]).slice(0, 3).map((district: string, idx: number) => (
57
+ <Badge key={idx} variant="outline" className="text-xs">
58
+ {String(district)}
59
+ </Badge>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </div>
64
+
65
+ {/* Active Alerts */}
66
+ {alerts.length > 0 && (
67
+ <div className="mb-2">
68
+ {alerts.slice(0, 2).map((alert, idx) => (
69
+ <div key={idx} className="p-2 rounded bg-destructive/10 border border-destructive/30 mb-1">
70
+ <div className="flex items-start gap-2">
71
+ <AlertTriangle className="w-3 h-3 text-destructive mt-0.5 flex-shrink-0" />
72
+ <p className="text-xs text-destructive">{alert.text}</p>
73
+ </div>
74
+ </div>
75
+ ))}
76
+ </div>
77
+ )}
78
+
79
+ {/* Advisories */}
80
+ {advisories.length > 0 && (
81
+ <div className="mb-2">
82
+ {advisories.slice(0, 2).map((adv, idx) => (
83
+ <div key={idx} className="p-2 rounded bg-warning/10 border border-warning/30 mb-1">
84
+ <div className="flex items-start gap-2">
85
+ <Activity className="w-3 h-3 text-warning mt-0.5 flex-shrink-0" />
86
+ <p className="text-xs text-warning">{adv.text}</p>
87
+ </div>
88
+ </div>
89
+ ))}
90
+ </div>
91
+ )}
92
+
93
+ {fetchedAt && (
94
+ <p className="text-xs text-muted-foreground mt-2">
95
+ Updated: {new Date(fetchedAt).toLocaleTimeString()}
96
+ </p>
97
+ )}
98
+ </Card>
99
+ );
100
+ };
101
+
102
+ export default HealthAlerts;
frontend/app/components/dashboard/HistoricalIntel.tsx CHANGED
@@ -170,7 +170,7 @@ export default function HistoricalIntel() {
170
  How Climate Has Changed
171
  </h4>
172
 
173
- <div className="overflow-x-auto">
174
  <table className="w-full text-sm">
175
  <thead>
176
  <tr className="border-b border-border">
@@ -221,8 +221,8 @@ export default function HistoricalIntel() {
221
  <Badge
222
  key={idx}
223
  className={`${period.risk === 'high'
224
- ? 'bg-destructive/20 text-destructive'
225
- : 'bg-warning/20 text-warning'
226
  }`}
227
  >
228
  {period.months}: {period.type}
 
170
  How Climate Has Changed
171
  </h4>
172
 
173
+ <div className="overflow-x-auto intel-scrollbar">
174
  <table className="w-full text-sm">
175
  <thead>
176
  <tr className="border-b border-border">
 
221
  <Badge
222
  key={idx}
223
  className={`${period.risk === 'high'
224
+ ? 'bg-destructive/20 text-destructive'
225
+ : 'bg-warning/20 text-warning'
226
  }`}
227
  >
228
  {period.months}: {period.type}
frontend/app/components/dashboard/PowerOutageStatus.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Zap, AlertTriangle, CheckCircle } from "lucide-react";
6
+
7
+ interface PowerStatusProps {
8
+ powerData?: Record<string, unknown> | null;
9
+ }
10
+
11
+ const PowerOutageStatus = ({ powerData }: PowerStatusProps) => {
12
+ const isActive = powerData?.load_shedding_active as boolean;
13
+ const status = (powerData?.status as string) || "unknown";
14
+ const announcements = (powerData?.announcements as string[]) || [];
15
+ const fetchedAt = powerData?.fetched_at as string;
16
+
17
+ const getStatusColor = () => {
18
+ if (status === "load_shedding") return "bg-destructive/20 text-destructive";
19
+ if (status === "operational" || status === "no_load_shedding") return "bg-success/20 text-success";
20
+ return "bg-muted/20 text-muted-foreground";
21
+ };
22
+
23
+ const getStatusLabel = () => {
24
+ if (status === "load_shedding") return "⚡ LOAD SHEDDING";
25
+ if (status === "operational" || status === "no_load_shedding") return "✓ NORMAL";
26
+ return "○ CHECKING...";
27
+ };
28
+
29
+ return (
30
+ <Card className="p-4 bg-card border-border">
31
+ <div className="flex items-center justify-between mb-3">
32
+ <div className="flex items-center gap-2">
33
+ <div className={`p-2 rounded-lg ${isActive ? 'bg-destructive/20' : 'bg-success/20'}`}>
34
+ <Zap className={`w-5 h-5 ${isActive ? 'text-destructive' : 'text-success'}`} />
35
+ </div>
36
+ <div>
37
+ <h3 className="font-bold text-sm">⚡ POWER STATUS</h3>
38
+ <p className="text-xs text-muted-foreground">CEB Sri Lanka</p>
39
+ </div>
40
+ </div>
41
+ <Badge className={getStatusColor()}>
42
+ {getStatusLabel()}
43
+ </Badge>
44
+ </div>
45
+
46
+ {isActive ? (
47
+ <div className="p-3 rounded-lg bg-destructive/10 border border-destructive/30 mb-2">
48
+ <div className="flex items-center gap-2 mb-1">
49
+ <AlertTriangle className="w-4 h-4 text-destructive" />
50
+ <span className="text-sm font-semibold text-destructive">Load Shedding Active</span>
51
+ </div>
52
+ <p className="text-xs text-destructive/80">Power cuts may be in effect in various areas</p>
53
+ </div>
54
+ ) : (
55
+ <div className="p-3 rounded-lg bg-success/10 border border-success/30 mb-2">
56
+ <div className="flex items-center gap-2">
57
+ <CheckCircle className="w-4 h-4 text-success" />
58
+ <span className="text-sm text-success">Normal power supply across the island</span>
59
+ </div>
60
+ </div>
61
+ )}
62
+
63
+ {announcements.length > 0 && (
64
+ <div className="mt-2">
65
+ {announcements.slice(0, 2).map((ann, idx) => (
66
+ <p key={idx} className="text-xs text-muted-foreground mb-1">• {ann}</p>
67
+ ))}
68
+ </div>
69
+ )}
70
+
71
+ {fetchedAt && (
72
+ <p className="text-xs text-muted-foreground mt-2">
73
+ Updated: {new Date(fetchedAt).toLocaleTimeString()}
74
+ </p>
75
+ )}
76
+ </Card>
77
+ );
78
+ };
79
+
80
+ export default PowerOutageStatus;
frontend/app/components/dashboard/StockPredictions.tsx CHANGED
@@ -146,7 +146,7 @@ const StockPredictions = () => {
146
  </button>
147
  </div>
148
  ) : stocks.length > 0 ? (
149
- <div className="space-y-2 max-h-[400px] overflow-y-auto pr-2">
150
  {stocks.map((stock, idx) => (
151
  <motion.div
152
  key={stock.symbol}
@@ -212,7 +212,7 @@ const StockPredictions = () => {
212
  </div>
213
 
214
  {marketEvents.length > 0 ? (
215
- <div className="space-y-2 max-h-[200px] overflow-y-auto">
216
  {marketEvents.slice(0, 5).map((event, idx) => (
217
  <motion.div
218
  key={event.event_id || idx}
 
146
  </button>
147
  </div>
148
  ) : stocks.length > 0 ? (
149
+ <div className="space-y-2 max-h-[400px] overflow-y-auto intel-scrollbar pr-2">
150
  {stocks.map((stock, idx) => (
151
  <motion.div
152
  key={stock.symbol}
 
212
  </div>
213
 
214
  {marketEvents.length > 0 ? (
215
+ <div className="space-y-2 max-h-[200px] overflow-y-auto intel-scrollbar pr-2">
216
  {marketEvents.slice(0, 5).map((event, idx) => (
217
  <motion.div
218
  key={event.event_id || idx}
frontend/app/components/dashboard/WaterSupplyStatus.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Droplets, AlertTriangle, CheckCircle } from "lucide-react";
6
+
7
+ interface WaterDisruption {
8
+ area: string;
9
+ type: string;
10
+ details: string;
11
+ severity: string;
12
+ }
13
+
14
+ interface WaterSupplyStatusProps {
15
+ waterData?: Record<string, unknown> | null;
16
+ }
17
+
18
+ const WaterSupplyStatus = ({ waterData }: WaterSupplyStatusProps) => {
19
+ const status = (waterData?.status as string) || "unknown";
20
+ const disruptions = (waterData?.active_disruptions as WaterDisruption[]) || [];
21
+ const overallSupply = waterData?.overall_supply as string;
22
+ const fetchedAt = waterData?.fetched_at as string;
23
+
24
+ const hasDisruptions = status === "disruptions_reported" || disruptions.length > 0;
25
+
26
+ return (
27
+ <Card className="p-4 bg-card border-border">
28
+ <div className="flex items-center justify-between mb-3">
29
+ <div className="flex items-center gap-2">
30
+ <div className={`p-2 rounded-lg ${hasDisruptions ? 'bg-warning/20' : 'bg-info/20'}`}>
31
+ <Droplets className={`w-5 h-5 ${hasDisruptions ? 'text-warning' : 'text-info'}`} />
32
+ </div>
33
+ <div>
34
+ <h3 className="font-bold text-sm">💧 WATER SUPPLY</h3>
35
+ <p className="text-xs text-muted-foreground">NWSDB Status</p>
36
+ </div>
37
+ </div>
38
+ <Badge className={hasDisruptions ? "bg-warning/20 text-warning" : "bg-success/20 text-success"}>
39
+ {hasDisruptions ? "⚠ DISRUPTIONS" : "✓ NORMAL"}
40
+ </Badge>
41
+ </div>
42
+
43
+ {hasDisruptions ? (
44
+ <div className="space-y-2">
45
+ {disruptions.slice(0, 3).map((d, idx) => (
46
+ <div key={idx} className="p-2 rounded bg-warning/10 border border-warning/30">
47
+ <div className="flex items-start gap-2">
48
+ <AlertTriangle className="w-3 h-3 text-warning mt-0.5 flex-shrink-0" />
49
+ <div className="flex-1">
50
+ <p className="text-sm font-medium text-warning">{d.area}</p>
51
+ <p className="text-xs text-warning/80">{d.type} - {d.details?.slice(0, 80)}...</p>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ ) : (
58
+ <div className="p-3 rounded-lg bg-success/10 border border-success/30">
59
+ <div className="flex items-center gap-2">
60
+ <CheckCircle className="w-4 h-4 text-success" />
61
+ <span className="text-sm text-success">
62
+ {overallSupply || "Normal water supply across most areas"}
63
+ </span>
64
+ </div>
65
+ </div>
66
+ )}
67
+
68
+ {fetchedAt && (
69
+ <p className="text-xs text-muted-foreground mt-3">
70
+ Updated: {new Date(fetchedAt).toLocaleTimeString()}
71
+ </p>
72
+ )}
73
+ </Card>
74
+ );
75
+ };
76
+
77
+ export default WaterSupplyStatus;
frontend/app/components/dashboard/WeatherPredictions.tsx CHANGED
@@ -163,7 +163,7 @@ export default function WeatherPredictions() {
163
  </div>
164
 
165
  {/* District Grid */}
166
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[500px] overflow-y-auto pr-2">
167
  {filteredDistricts.map(([district, pred]) => (
168
  <div
169
  key={district}
@@ -213,11 +213,6 @@ export default function WeatherPredictions() {
213
  <span className="text-slate-400">Station:</span>
214
  <span className="text-white">{pred.station_used}</span>
215
  </div>
216
- {pred.is_fallback && (
217
- <div className="text-xs text-yellow-400">
218
- ⚠️ Using climate fallback (LSTM model not trained)
219
- </div>
220
- )}
221
  </div>
222
  )}
223
  </div>
 
163
  </div>
164
 
165
  {/* District Grid */}
166
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[500px] overflow-y-auto intel-scrollbar pr-2">
167
  {filteredDistricts.map(([district, pred]) => (
168
  <div
169
  key={district}
 
213
  <span className="text-slate-400">Station:</span>
214
  <span className="text-white">{pred.station_used}</span>
215
  </div>
 
 
 
 
 
216
  </div>
217
  )}
218
  </div>
frontend/app/components/map/DistrictInfoPanel.tsx CHANGED
@@ -146,7 +146,7 @@ const DistrictInfoPanel = ({ district }: DistrictInfoPanelProps) => {
146
  exit={{ opacity: 0, x: -20 }}
147
  transition={{ duration: 0.3 }}
148
  >
149
- <Card className="p-4 sm:p-6 bg-card border-border space-y-4 max-h-[60vh] sm:max-h-none overflow-y-auto hide-scrollbar">
150
  {/* Header */}
151
  <div className="sticky top-0 bg-card z-10 pb-2 border-b border-border/50">
152
  <div className="flex items-center justify-between mb-2">
 
146
  exit={{ opacity: 0, x: -20 }}
147
  transition={{ duration: 0.3 }}
148
  >
149
+ <Card className="p-4 sm:p-6 bg-card border-border space-y-4 max-h-[60vh] sm:max-h-none overflow-y-auto intel-scrollbar">
150
  {/* Header */}
151
  <div className="sticky top-0 bg-card z-10 pb-2 border-b border-border/50">
152
  <div className="flex items-center justify-between mb-2">
frontend/app/hooks/use-roger-data.ts CHANGED
@@ -305,11 +305,58 @@ export function useRogerData() {
305
  return () => clearInterval(interval);
306
  }, [fetchRiverData]);
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  return {
309
  ...state,
310
  isConnected,
311
  events: state.final_ranked_feed,
312
  dashboard: state.risk_dashboard_snapshot,
313
- riverData // NEW: Expose river data for RiverNetStatus component
 
 
 
 
 
 
 
314
  };
315
  }
 
305
  return () => clearInterval(interval);
306
  }, [fetchRiverData]);
307
 
308
+ // ============================================
309
+ // SITUATIONAL AWARENESS DATA (NEW)
310
+ // ============================================
311
+ const [powerData, setPowerData] = useState<Record<string, unknown> | null>(null);
312
+ const [fuelData, setFuelData] = useState<Record<string, unknown> | null>(null);
313
+ const [economyData, setEconomyData] = useState<Record<string, unknown> | null>(null);
314
+ const [healthData, setHealthData] = useState<Record<string, unknown> | null>(null);
315
+ const [commodityData, setCommodityData] = useState<Record<string, unknown> | null>(null);
316
+ const [waterData, setWaterData] = useState<Record<string, unknown> | null>(null);
317
+
318
+ // Fetch situational awareness data
319
+ const fetchSituationalData = useCallback(async () => {
320
+ try {
321
+ const [powerRes, fuelRes, economyRes, healthRes, commodityRes, waterRes] = await Promise.all([
322
+ fetch(`${API_BASE}/api/power`).catch(() => null),
323
+ fetch(`${API_BASE}/api/fuel`).catch(() => null),
324
+ fetch(`${API_BASE}/api/economy`).catch(() => null),
325
+ fetch(`${API_BASE}/api/health`).catch(() => null),
326
+ fetch(`${API_BASE}/api/commodities`).catch(() => null),
327
+ fetch(`${API_BASE}/api/water`).catch(() => null),
328
+ ]);
329
+
330
+ if (powerRes?.ok) setPowerData(await powerRes.json());
331
+ if (fuelRes?.ok) setFuelData(await fuelRes.json());
332
+ if (economyRes?.ok) setEconomyData(await economyRes.json());
333
+ if (healthRes?.ok) setHealthData(await healthRes.json());
334
+ if (commodityRes?.ok) setCommodityData(await commodityRes.json());
335
+ if (waterRes?.ok) setWaterData(await waterRes.json());
336
+ } catch (err) {
337
+ console.warn('[Roger] Failed to fetch situational data:', err);
338
+ }
339
+ }, []);
340
+
341
+ // Fetch situational data periodically (every 5 minutes)
342
+ useEffect(() => {
343
+ fetchSituationalData();
344
+ const interval = setInterval(fetchSituationalData, 300000); // Every 5 min
345
+ return () => clearInterval(interval);
346
+ }, [fetchSituationalData]);
347
+
348
  return {
349
  ...state,
350
  isConnected,
351
  events: state.final_ranked_feed,
352
  dashboard: state.risk_dashboard_snapshot,
353
+ riverData,
354
+ // NEW: Situational awareness data
355
+ powerData,
356
+ fuelData,
357
+ economyData,
358
+ healthData,
359
+ commodityData,
360
+ waterData,
361
  };
362
  }
main.py CHANGED
@@ -767,6 +767,149 @@ def get_national_threat_score():
767
  "error": str(e)
768
  }
769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
 
771
  # NOTE: Weather predictions endpoint moved to async version below (line ~1540)
772
  # NOTE: Currency prediction endpoint moved to async version below (line ~1680)
 
767
  "error": str(e)
768
  }
769
 
770
+ # ============================================
771
+ # SITUATIONAL AWARENESS API ENDPOINTS (NEW)
772
+ # ============================================
773
+
774
+ @app.get("/api/power")
775
+ def get_power_status():
776
+ """
777
+ Get CEB power outage / load shedding status.
778
+
779
+ Returns current power supply status, active load shedding schedules,
780
+ and any CEB announcements.
781
+ """
782
+ try:
783
+ from src.utils.utils import tool_ceb_power_status
784
+ power_data = tool_ceb_power_status()
785
+ return {
786
+ "status": "success",
787
+ **power_data
788
+ }
789
+ except Exception as e:
790
+ logger.error(f"[API] Error fetching power status: {e}")
791
+ return {
792
+ "status": "error",
793
+ "load_shedding_active": False,
794
+ "error": str(e)
795
+ }
796
+
797
+
798
+ @app.get("/api/fuel")
799
+ def get_fuel_prices():
800
+ """
801
+ Get current fuel prices in Sri Lanka.
802
+
803
+ Returns prices for Petrol 92/95, Diesel, Super Diesel, and Kerosene.
804
+ """
805
+ try:
806
+ from src.utils.utils import tool_fuel_prices
807
+ fuel_data = tool_fuel_prices()
808
+ return {
809
+ "status": "success",
810
+ **fuel_data
811
+ }
812
+ except Exception as e:
813
+ logger.error(f"[API] Error fetching fuel prices: {e}")
814
+ return {
815
+ "status": "error",
816
+ "prices": {},
817
+ "error": str(e)
818
+ }
819
+
820
+
821
+ @app.get("/api/economy")
822
+ def get_economic_indicators():
823
+ """
824
+ Get key economic indicators from CBSL.
825
+
826
+ Returns inflation rates, policy rates, exchange rates, and forex reserves.
827
+ """
828
+ try:
829
+ from src.utils.utils import tool_cbsl_indicators
830
+ economy_data = tool_cbsl_indicators()
831
+ return {
832
+ "status": "success",
833
+ **economy_data
834
+ }
835
+ except Exception as e:
836
+ logger.error(f"[API] Error fetching economic indicators: {e}")
837
+ return {
838
+ "status": "error",
839
+ "indicators": {},
840
+ "error": str(e)
841
+ }
842
+
843
+
844
+ @app.get("/api/health")
845
+ def get_health_alerts():
846
+ """
847
+ Get health alerts and disease information.
848
+
849
+ Returns current health alerts, dengue case data, and health advisories.
850
+ """
851
+ try:
852
+ from src.utils.utils import tool_health_alerts
853
+ health_data = tool_health_alerts()
854
+ return {
855
+ "status": "success",
856
+ **health_data
857
+ }
858
+ except Exception as e:
859
+ logger.error(f"[API] Error fetching health data: {e}")
860
+ return {
861
+ "status": "error",
862
+ "alerts": [],
863
+ "dengue": {},
864
+ "error": str(e)
865
+ }
866
+
867
+
868
+ @app.get("/api/commodities")
869
+ def get_commodity_prices():
870
+ """
871
+ Get prices for essential commodities.
872
+
873
+ Returns current prices for rice, sugar, dhal, milk powder, and other staples.
874
+ """
875
+ try:
876
+ from src.utils.utils import tool_commodity_prices
877
+ commodity_data = tool_commodity_prices()
878
+ return {
879
+ "status": "success",
880
+ **commodity_data
881
+ }
882
+ except Exception as e:
883
+ logger.error(f"[API] Error fetching commodity prices: {e}")
884
+ return {
885
+ "status": "error",
886
+ "commodities": [],
887
+ "error": str(e)
888
+ }
889
+
890
+
891
+ @app.get("/api/water")
892
+ def get_water_supply_status():
893
+ """
894
+ Get water supply disruption alerts from NWSDB.
895
+
896
+ Returns active disruptions, affected areas, and restoration estimates.
897
+ """
898
+ try:
899
+ from src.utils.utils import tool_water_supply_alerts
900
+ water_data = tool_water_supply_alerts()
901
+ return {
902
+ "status": "success",
903
+ **water_data
904
+ }
905
+ except Exception as e:
906
+ logger.error(f"[API] Error fetching water status: {e}")
907
+ return {
908
+ "status": "error",
909
+ "active_disruptions": [],
910
+ "error": str(e)
911
+ }
912
+
913
 
914
  # NOTE: Weather predictions endpoint moved to async version below (line ~1540)
915
  # NOTE: Currency prediction endpoint moved to async version below (line ~1680)
src/config/intel_config.json CHANGED
@@ -1,23 +1,33 @@
1
  {
2
  "user_profiles": {
3
  "twitter": [
4
- "nivakaran"
 
 
5
  ],
6
  "facebook": [
7
- "Nivakaran"
 
 
8
  ],
9
  "linkedin": [
10
- "nivakaran"
 
 
 
11
  ]
12
  },
13
  "user_keywords": [
14
  "Colombo",
15
  "nivakaran",
16
- "telco"
 
17
  ],
18
  "user_products": [
19
  "iphone",
20
- "anchor"
 
 
21
  ],
22
  "operational_keywords": {
23
  "infrastructure": [
 
1
  {
2
  "user_profiles": {
3
  "twitter": [
4
+ "nivakaran",
5
+ "iit",
6
+ "model-x"
7
  ],
8
  "facebook": [
9
+ "Nivakaran",
10
+ "iit",
11
+ "sliit"
12
  ],
13
  "linkedin": [
14
+ "nivakaran",
15
+ "ieee",
16
+ "sliit",
17
+ "albert"
18
  ]
19
  },
20
  "user_keywords": [
21
  "Colombo",
22
  "nivakaran",
23
+ "model-x",
24
+ "Colombo port"
25
  ],
26
  "user_products": [
27
  "iphone",
28
+ "anchor",
29
+ "iphone xr",
30
+ "iphone 13 pro"
31
  ],
32
  "operational_keywords": {
33
  "infrastructure": [
src/graphs/RogerGraph.py CHANGED
@@ -1,25 +1,19 @@
1
  """
2
- src/graphs/RogerGraph.py
3
- COMPLETE - Main Roger Graph with Fan-Out/Fan-In Architecture
4
- This is the "Mother Graph" that orchestrates all domain agents
5
  """
6
 
7
  from __future__ import annotations
8
  import logging
9
  from langgraph.graph import StateGraph, START, END
10
 
11
- # State and Node imports
12
  from src.states.combinedAgentState import CombinedAgentState
13
  from src.nodes.combinedAgentNode import CombinedAgentNode
14
-
15
- # Domain graph builders
16
  from src.graphs.dataRetrievalAgentGraph import DataRetrievalAgentGraph
17
  from src.graphs.meteorologicalAgentGraph import MeteorologicalGraphBuilder
18
  from src.graphs.politicalAgentGraph import PoliticalGraphBuilder
19
  from src.graphs.economicalAgentGraph import EconomicalGraphBuilder
20
  from src.graphs.intelligenceAgentGraph import IntelligenceGraphBuilder
21
  from src.graphs.socialAgentGraph import SocialGraphBuilder
22
-
23
  from src.llms.groqllm import GroqLLM
24
 
25
  logger = logging.getLogger("Roger_graph")
@@ -31,26 +25,12 @@ if not logger.handlers:
31
 
32
 
33
  class CombinedAgentGraphBuilder:
34
- """
35
- Builds the main Roger graph implementing Fan-Out/Fan-In architecture.
36
-
37
- Architecture:
38
- 1. GraphInitiator (START)
39
- 2. Fan-Out to 6 Domain Agents (parallel execution)
40
- 3. Fan-In to FeedAggregator (collects domain_insights)
41
- 4. DataRefresher (updates dashboard)
42
- 5. DataRefreshRouter (loop or end decision)
43
- """
44
-
45
  def __init__(self, llm):
46
  self.llm = llm
47
 
48
  def build_graph(self):
49
- logger.info("=" * 60)
50
- logger.info("BUILDING Roger COMBINED AGENT GRAPH")
51
- logger.info("=" * 60)
52
 
53
- # 1. Instantiate domain graph builders
54
  social_builder = SocialGraphBuilder(self.llm)
55
  intelligence_builder = IntelligenceGraphBuilder(self.llm)
56
  economical_builder = EconomicalGraphBuilder(self.llm)
@@ -58,39 +38,23 @@ class CombinedAgentGraphBuilder:
58
  meteorological_builder = MeteorologicalGraphBuilder(self.llm)
59
  data_retrieval_builder = DataRetrievalAgentGraph(self.llm)
60
 
61
- logger.info("✓ Domain graph builders instantiated")
62
-
63
- # 2. Instantiate orchestration node
64
  orchestrator = CombinedAgentNode(self.llm)
65
- logger.info("✓ Orchestration node instantiated")
66
-
67
- # 3. Create state graph with CombinedAgentState
68
  workflow = StateGraph(CombinedAgentState)
69
- logger.info("✓ StateGraph created with CombinedAgentState")
70
 
71
- # 4. Add orchestration nodes
72
  workflow.add_node("GraphInitiator", orchestrator.graph_initiator)
73
  workflow.add_node("FeedAggregatorAgent", orchestrator.feed_aggregator_agent)
74
  workflow.add_node("DataRefresherAgent", orchestrator.data_refresher_agent)
75
  workflow.add_node("DataRefreshRouter", orchestrator.data_refresh_router)
76
- logger.info("✓ Orchestration nodes added")
77
 
78
- # 5. Add domain subgraphs (compiled graphs as nodes)
79
  workflow.add_node("SocialAgent", social_builder.build_graph())
80
  workflow.add_node("IntelligenceAgent", intelligence_builder.build_graph())
81
  workflow.add_node("EconomicalAgent", economical_builder.build_graph())
82
  workflow.add_node("PoliticalAgent", political_builder.build_graph())
83
  workflow.add_node("MeteorologicalAgent", meteorological_builder.build_graph())
84
- workflow.add_node(
85
- "DataRetrievalAgent",
86
- data_retrieval_builder.build_data_retrieval_agent_graph(),
87
- )
88
- logger.info("✓ Domain agent subgraphs added")
89
 
90
- # 6. Wire the graph: START -> Initiator
91
  workflow.add_edge(START, "GraphInitiator")
92
 
93
- # 7. Fan-Out: Initiator -> All Domain Agents (parallel execution)
94
  domain_agents = [
95
  "SocialAgent",
96
  "IntelligenceAgent",
@@ -103,40 +67,18 @@ class CombinedAgentGraphBuilder:
103
  for agent in domain_agents:
104
  workflow.add_edge("GraphInitiator", agent)
105
 
106
- logger.info(
107
- f"✓ Fan-Out configured: GraphInitiator -> {len(domain_agents)} agents"
108
- )
109
-
110
- # 8. Fan-In: All Domain Agents -> FeedAggregator
111
  for agent in domain_agents:
112
  workflow.add_edge(agent, "FeedAggregatorAgent")
113
 
114
- logger.info(
115
- f"✓ Fan-In configured: {len(domain_agents)} agents -> FeedAggregator"
116
- )
117
-
118
- # 9. Linear flow: Aggregator -> Refresher -> Router
119
  workflow.add_edge("FeedAggregatorAgent", "DataRefresherAgent")
120
  workflow.add_edge("DataRefresherAgent", "DataRefreshRouter")
121
- logger.info("✓ Linear orchestration flow configured")
122
 
123
- # 10. Conditional routing: Router -> Loop or END
124
  def route_decision(state):
125
- """
126
- Router function for conditional edges.
127
- Returns the next node name or END.
128
- """
129
  route = getattr(state, "route", [])
130
-
131
- # If route is None or empty, go to END
132
  if route is None or route == "":
133
  return END
134
-
135
- # If route is "GraphInitiator", loop back
136
  if route == "GraphInitiator":
137
  return "GraphInitiator"
138
-
139
- # Default to END
140
  return END
141
 
142
  workflow.add_conditional_edges(
@@ -144,42 +86,12 @@ class CombinedAgentGraphBuilder:
144
  route_decision,
145
  {"GraphInitiator": "GraphInitiator", END: END},
146
  )
147
- logger.info("✓ Conditional routing configured")
148
 
149
- # 11. Compile the graph
150
  graph = workflow.compile()
151
-
152
- logger.info("=" * 60)
153
- logger.info("✓ Roger GRAPH COMPILED SUCCESSFULLY")
154
- logger.info("=" * 60)
155
- logger.info("")
156
- logger.info("Graph Structure:")
157
- logger.info(" START")
158
- logger.info(" ↓")
159
- logger.info(" GraphInitiator")
160
- logger.info(" ↓↓↓↓↓↓ (Fan-Out)")
161
- logger.info(
162
- " [Social, Intelligence, Economic, Political, Meteorological, DataRetrieval]"
163
- )
164
- logger.info(" ↓↓↓↓↓↓ (Fan-In)")
165
- logger.info(" FeedAggregatorAgent")
166
- logger.info(" ↓")
167
- logger.info(" DataRefresherAgent")
168
- logger.info(" ↓")
169
- logger.info(" DataRefreshRouter")
170
- logger.info(" ↓ (conditional)")
171
- logger.info(" [GraphInitiator (loop) OR END]")
172
- logger.info("")
173
-
174
  return graph
175
 
176
 
177
- # Module-level compilation for LangGraph CLI
178
- print("\n" + "=" * 60)
179
- print("INITIALIZING Roger PLATFORM")
180
- print("=" * 60)
181
  llm = GroqLLM().get_llm()
182
  builder = CombinedAgentGraphBuilder(llm)
183
  graph = builder.build_graph()
184
- print("\n✓ Roger Platform Ready")
185
- print("=" * 60)
 
1
  """
2
+ RogerGraph.py - Main Roger Graph with Fan-Out/Fan-In Architecture
 
 
3
  """
4
 
5
  from __future__ import annotations
6
  import logging
7
  from langgraph.graph import StateGraph, START, END
8
 
 
9
  from src.states.combinedAgentState import CombinedAgentState
10
  from src.nodes.combinedAgentNode import CombinedAgentNode
 
 
11
  from src.graphs.dataRetrievalAgentGraph import DataRetrievalAgentGraph
12
  from src.graphs.meteorologicalAgentGraph import MeteorologicalGraphBuilder
13
  from src.graphs.politicalAgentGraph import PoliticalGraphBuilder
14
  from src.graphs.economicalAgentGraph import EconomicalGraphBuilder
15
  from src.graphs.intelligenceAgentGraph import IntelligenceGraphBuilder
16
  from src.graphs.socialAgentGraph import SocialGraphBuilder
 
17
  from src.llms.groqllm import GroqLLM
18
 
19
  logger = logging.getLogger("Roger_graph")
 
25
 
26
 
27
  class CombinedAgentGraphBuilder:
 
 
 
 
 
 
 
 
 
 
 
28
  def __init__(self, llm):
29
  self.llm = llm
30
 
31
  def build_graph(self):
32
+ logger.info("Building Roger Combined Agent Graph")
 
 
33
 
 
34
  social_builder = SocialGraphBuilder(self.llm)
35
  intelligence_builder = IntelligenceGraphBuilder(self.llm)
36
  economical_builder = EconomicalGraphBuilder(self.llm)
 
38
  meteorological_builder = MeteorologicalGraphBuilder(self.llm)
39
  data_retrieval_builder = DataRetrievalAgentGraph(self.llm)
40
 
 
 
 
41
  orchestrator = CombinedAgentNode(self.llm)
 
 
 
42
  workflow = StateGraph(CombinedAgentState)
 
43
 
 
44
  workflow.add_node("GraphInitiator", orchestrator.graph_initiator)
45
  workflow.add_node("FeedAggregatorAgent", orchestrator.feed_aggregator_agent)
46
  workflow.add_node("DataRefresherAgent", orchestrator.data_refresher_agent)
47
  workflow.add_node("DataRefreshRouter", orchestrator.data_refresh_router)
 
48
 
 
49
  workflow.add_node("SocialAgent", social_builder.build_graph())
50
  workflow.add_node("IntelligenceAgent", intelligence_builder.build_graph())
51
  workflow.add_node("EconomicalAgent", economical_builder.build_graph())
52
  workflow.add_node("PoliticalAgent", political_builder.build_graph())
53
  workflow.add_node("MeteorologicalAgent", meteorological_builder.build_graph())
54
+ workflow.add_node("DataRetrievalAgent", data_retrieval_builder.build_data_retrieval_agent_graph())
 
 
 
 
55
 
 
56
  workflow.add_edge(START, "GraphInitiator")
57
 
 
58
  domain_agents = [
59
  "SocialAgent",
60
  "IntelligenceAgent",
 
67
  for agent in domain_agents:
68
  workflow.add_edge("GraphInitiator", agent)
69
 
 
 
 
 
 
70
  for agent in domain_agents:
71
  workflow.add_edge(agent, "FeedAggregatorAgent")
72
 
 
 
 
 
 
73
  workflow.add_edge("FeedAggregatorAgent", "DataRefresherAgent")
74
  workflow.add_edge("DataRefresherAgent", "DataRefreshRouter")
 
75
 
 
76
  def route_decision(state):
 
 
 
 
77
  route = getattr(state, "route", [])
 
 
78
  if route is None or route == "":
79
  return END
 
 
80
  if route == "GraphInitiator":
81
  return "GraphInitiator"
 
 
82
  return END
83
 
84
  workflow.add_conditional_edges(
 
86
  route_decision,
87
  {"GraphInitiator": "GraphInitiator", END: END},
88
  )
 
89
 
 
90
  graph = workflow.compile()
91
+ logger.info("Roger Graph compiled successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  return graph
93
 
94
 
 
 
 
 
95
  llm = GroqLLM().get_llm()
96
  builder = CombinedAgentGraphBuilder(llm)
97
  graph = builder.build_graph()
 
 
src/graphs/combinedAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- combinedAgentGraph.py
3
- Main entry point for the Combined Agent System.
4
- FIXED: Removed sub-graph wrappers that were causing CancelledError
5
  """
6
 
7
  from __future__ import annotations
@@ -9,32 +7,25 @@ from typing import Dict, Any
9
  import logging
10
  from datetime import datetime
11
 
12
- # LangGraph Imports
13
  from langgraph.graph import StateGraph, START, END
14
 
15
- # Project Imports
16
  from src.llms.groqllm import GroqLLM
17
  from src.states.combinedAgentState import CombinedAgentState
18
  from src.nodes.combinedAgentNode import CombinedAgentNode
19
 
20
- # LangSmith Tracing (auto-configures if LANGSMITH_API_KEY is set)
21
  try:
22
  from src.config.langsmith_config import LangSmithConfig
23
-
24
  _langsmith = LangSmithConfig()
25
  _langsmith.configure()
26
  except ImportError:
27
- pass # LangSmith not installed, tracing disabled
28
-
29
 
30
- # Import Sub-Graph Builders
31
  from src.graphs.socialAgentGraph import SocialGraphBuilder
32
  from src.graphs.intelligenceAgentGraph import IntelligenceGraphBuilder
33
  from src.graphs.economicalAgentGraph import EconomicalGraphBuilder
34
  from src.graphs.politicalAgentGraph import PoliticalGraphBuilder
35
  from src.graphs.meteorologicalAgentGraph import MeteorologicalGraphBuilder
36
 
37
- # Configure Logging
38
  logger = logging.getLogger("main_graph")
39
  logger.setLevel(logging.INFO)
40
  if not logger.handlers:
@@ -48,114 +39,83 @@ class CombinedAgentGraphBuilder:
48
  self.llm = llm
49
 
50
  def build_graph(self):
51
- # 1. Initialize Sub-Graph Builders and compile them
52
  social_graph = SocialGraphBuilder(self.llm).build_graph()
53
  intelligence_graph = IntelligenceGraphBuilder(self.llm).build_graph()
54
  economical_graph = EconomicalGraphBuilder(self.llm).build_graph()
55
  political_graph = PoliticalGraphBuilder(self.llm).build_graph()
56
  meteorological_graph = MeteorologicalGraphBuilder(self.llm).build_graph()
57
 
58
- # 2. Create wrapper functions to extract domain_insights from sub-agent states
59
- # This solves the state type mismatch issue - sub-agents return their own state types
60
- # but we need to update CombinedAgentState. Wrappers extract domain_insights and
61
- # return update dicts that get merged via the reduce_insights reducer.
62
-
63
  def run_social_agent(state: CombinedAgentState) -> Dict[str, Any]:
64
- """Wrapper to invoke SocialAgent and extract domain_insights"""
65
  logger.info("[CombinedGraph] Invoking SocialAgent...")
66
  try:
67
  result = social_graph.invoke({})
68
  insights = result.get("domain_insights", [])
69
- logger.info(
70
- f"[CombinedGraph] SocialAgent returned {len(insights)} insights"
71
- )
72
  return {"domain_insights": insights}
73
  except Exception as e:
74
  logger.error(f"[CombinedGraph] SocialAgent FAILED: {e}")
75
- return {"domain_insights": []} # Graceful degradation
76
 
77
  def run_intelligence_agent(state: CombinedAgentState) -> Dict[str, Any]:
78
- """Wrapper to invoke IntelligenceAgent and extract domain_insights"""
79
  logger.info("[CombinedGraph] Invoking IntelligenceAgent...")
80
  try:
81
  result = intelligence_graph.invoke({})
82
  insights = result.get("domain_insights", [])
83
- logger.info(
84
- f"[CombinedGraph] IntelligenceAgent returned {len(insights)} insights"
85
- )
86
  return {"domain_insights": insights}
87
  except Exception as e:
88
  logger.error(f"[CombinedGraph] IntelligenceAgent FAILED: {e}")
89
- return {"domain_insights": []} # Graceful degradation
90
 
91
  def run_economical_agent(state: CombinedAgentState) -> Dict[str, Any]:
92
- """Wrapper to invoke EconomicalAgent and extract domain_insights"""
93
  logger.info("[CombinedGraph] Invoking EconomicalAgent...")
94
  try:
95
  result = economical_graph.invoke({})
96
  insights = result.get("domain_insights", [])
97
- logger.info(
98
- f"[CombinedGraph] EconomicalAgent returned {len(insights)} insights"
99
- )
100
  return {"domain_insights": insights}
101
  except Exception as e:
102
  logger.error(f"[CombinedGraph] EconomicalAgent FAILED: {e}")
103
- return {"domain_insights": []} # Graceful degradation
104
 
105
  def run_political_agent(state: CombinedAgentState) -> Dict[str, Any]:
106
- """Wrapper to invoke PoliticalAgent and extract domain_insights"""
107
  logger.info("[CombinedGraph] Invoking PoliticalAgent...")
108
  try:
109
  result = political_graph.invoke({})
110
  insights = result.get("domain_insights", [])
111
- logger.info(
112
- f"[CombinedGraph] PoliticalAgent returned {len(insights)} insights"
113
- )
114
  return {"domain_insights": insights}
115
  except Exception as e:
116
  logger.error(f"[CombinedGraph] PoliticalAgent FAILED: {e}")
117
- return {"domain_insights": []} # Graceful degradation
118
 
119
  def run_meteorological_agent(state: CombinedAgentState) -> Dict[str, Any]:
120
- """Wrapper to invoke MeteorologicalAgent and extract domain_insights"""
121
  logger.info("[CombinedGraph] Invoking MeteorologicalAgent...")
122
  try:
123
  result = meteorological_graph.invoke({})
124
  insights = result.get("domain_insights", [])
125
- logger.info(
126
- f"[CombinedGraph] MeteorologicalAgent returned {len(insights)} insights"
127
- )
128
  return {"domain_insights": insights}
129
  except Exception as e:
130
  logger.error(f"[CombinedGraph] MeteorologicalAgent FAILED: {e}")
131
- return {"domain_insights": []} # Graceful degradation
132
 
133
- # 3. Initialize Main Orchestrator Node
134
  orchestrator = CombinedAgentNode(self.llm)
135
-
136
- # 4. Create State Graph
137
  workflow = StateGraph(CombinedAgentState)
138
 
139
- # 5. Add Sub-Agent Wrapper Nodes
140
- # These wrappers extract domain_insights from sub-agent results and
141
- # return updates for CombinedAgentState (via the reduce_insights reducer)
142
  workflow.add_node("SocialAgent", run_social_agent)
143
  workflow.add_node("IntelligenceAgent", run_intelligence_agent)
144
  workflow.add_node("EconomicalAgent", run_economical_agent)
145
  workflow.add_node("PoliticalAgent", run_political_agent)
146
  workflow.add_node("MeteorologicalAgent", run_meteorological_agent)
147
 
148
- # 6. Add Orchestration Nodes (Fan-In)
149
  workflow.add_node("GraphInitiator", orchestrator.graph_initiator)
150
  workflow.add_node("FeedAggregatorAgent", orchestrator.feed_aggregator_agent)
151
  workflow.add_node("DataRefresherAgent", orchestrator.data_refresher_agent)
152
  workflow.add_node("DataRefreshRouter", orchestrator.data_refresh_router)
153
 
154
- # 7. Define Edges
155
- # Start -> Initiator
156
  workflow.add_edge(START, "GraphInitiator")
157
 
158
- # Initiator -> All Sub-Agents (Parallel)
159
  sub_agents = [
160
  "SocialAgent",
161
  "IntelligenceAgent",
@@ -167,11 +127,9 @@ class CombinedAgentGraphBuilder:
167
  workflow.add_edge("GraphInitiator", agent)
168
  workflow.add_edge(agent, "FeedAggregatorAgent")
169
 
170
- # Aggregator -> Refresher -> Router
171
  workflow.add_edge("FeedAggregatorAgent", "DataRefresherAgent")
172
  workflow.add_edge("DataRefresherAgent", "DataRefreshRouter")
173
 
174
- # 8. Conditional Routing
175
  workflow.add_conditional_edges(
176
  "DataRefreshRouter",
177
  lambda x: x.route if x.route else "END",
@@ -181,11 +139,8 @@ class CombinedAgentGraphBuilder:
181
  return workflow.compile()
182
 
183
 
184
- # --- GLOBAL EXPORT FOR LANGGRAPH DEV ---
185
- # This code runs when the file is imported.
186
- # It instantiates the LLM and builds the graph object.
187
- print("--- BUILDING COMBINED AGENT GRAPH (FIXED: State Sync Wrappers) ---")
188
  llm = GroqLLM().get_llm()
189
  builder = CombinedAgentGraphBuilder(llm)
190
  graph = builder.build_graph()
191
- print("Combined Roger Graph built successfully")
 
1
  """
2
+ combinedAgentGraph.py - Main entry point for the Combined Agent System.
 
 
3
  """
4
 
5
  from __future__ import annotations
 
7
  import logging
8
  from datetime import datetime
9
 
 
10
  from langgraph.graph import StateGraph, START, END
11
 
 
12
  from src.llms.groqllm import GroqLLM
13
  from src.states.combinedAgentState import CombinedAgentState
14
  from src.nodes.combinedAgentNode import CombinedAgentNode
15
 
 
16
  try:
17
  from src.config.langsmith_config import LangSmithConfig
 
18
  _langsmith = LangSmithConfig()
19
  _langsmith.configure()
20
  except ImportError:
21
+ pass
 
22
 
 
23
  from src.graphs.socialAgentGraph import SocialGraphBuilder
24
  from src.graphs.intelligenceAgentGraph import IntelligenceGraphBuilder
25
  from src.graphs.economicalAgentGraph import EconomicalGraphBuilder
26
  from src.graphs.politicalAgentGraph import PoliticalGraphBuilder
27
  from src.graphs.meteorologicalAgentGraph import MeteorologicalGraphBuilder
28
 
 
29
  logger = logging.getLogger("main_graph")
30
  logger.setLevel(logging.INFO)
31
  if not logger.handlers:
 
39
  self.llm = llm
40
 
41
  def build_graph(self):
 
42
  social_graph = SocialGraphBuilder(self.llm).build_graph()
43
  intelligence_graph = IntelligenceGraphBuilder(self.llm).build_graph()
44
  economical_graph = EconomicalGraphBuilder(self.llm).build_graph()
45
  political_graph = PoliticalGraphBuilder(self.llm).build_graph()
46
  meteorological_graph = MeteorologicalGraphBuilder(self.llm).build_graph()
47
 
 
 
 
 
 
48
  def run_social_agent(state: CombinedAgentState) -> Dict[str, Any]:
 
49
  logger.info("[CombinedGraph] Invoking SocialAgent...")
50
  try:
51
  result = social_graph.invoke({})
52
  insights = result.get("domain_insights", [])
53
+ logger.info(f"[CombinedGraph] SocialAgent returned {len(insights)} insights")
 
 
54
  return {"domain_insights": insights}
55
  except Exception as e:
56
  logger.error(f"[CombinedGraph] SocialAgent FAILED: {e}")
57
+ return {"domain_insights": []}
58
 
59
  def run_intelligence_agent(state: CombinedAgentState) -> Dict[str, Any]:
 
60
  logger.info("[CombinedGraph] Invoking IntelligenceAgent...")
61
  try:
62
  result = intelligence_graph.invoke({})
63
  insights = result.get("domain_insights", [])
64
+ logger.info(f"[CombinedGraph] IntelligenceAgent returned {len(insights)} insights")
 
 
65
  return {"domain_insights": insights}
66
  except Exception as e:
67
  logger.error(f"[CombinedGraph] IntelligenceAgent FAILED: {e}")
68
+ return {"domain_insights": []}
69
 
70
  def run_economical_agent(state: CombinedAgentState) -> Dict[str, Any]:
 
71
  logger.info("[CombinedGraph] Invoking EconomicalAgent...")
72
  try:
73
  result = economical_graph.invoke({})
74
  insights = result.get("domain_insights", [])
75
+ logger.info(f"[CombinedGraph] EconomicalAgent returned {len(insights)} insights")
 
 
76
  return {"domain_insights": insights}
77
  except Exception as e:
78
  logger.error(f"[CombinedGraph] EconomicalAgent FAILED: {e}")
79
+ return {"domain_insights": []}
80
 
81
  def run_political_agent(state: CombinedAgentState) -> Dict[str, Any]:
 
82
  logger.info("[CombinedGraph] Invoking PoliticalAgent...")
83
  try:
84
  result = political_graph.invoke({})
85
  insights = result.get("domain_insights", [])
86
+ logger.info(f"[CombinedGraph] PoliticalAgent returned {len(insights)} insights")
 
 
87
  return {"domain_insights": insights}
88
  except Exception as e:
89
  logger.error(f"[CombinedGraph] PoliticalAgent FAILED: {e}")
90
+ return {"domain_insights": []}
91
 
92
  def run_meteorological_agent(state: CombinedAgentState) -> Dict[str, Any]:
 
93
  logger.info("[CombinedGraph] Invoking MeteorologicalAgent...")
94
  try:
95
  result = meteorological_graph.invoke({})
96
  insights = result.get("domain_insights", [])
97
+ logger.info(f"[CombinedGraph] MeteorologicalAgent returned {len(insights)} insights")
 
 
98
  return {"domain_insights": insights}
99
  except Exception as e:
100
  logger.error(f"[CombinedGraph] MeteorologicalAgent FAILED: {e}")
101
+ return {"domain_insights": []}
102
 
 
103
  orchestrator = CombinedAgentNode(self.llm)
 
 
104
  workflow = StateGraph(CombinedAgentState)
105
 
 
 
 
106
  workflow.add_node("SocialAgent", run_social_agent)
107
  workflow.add_node("IntelligenceAgent", run_intelligence_agent)
108
  workflow.add_node("EconomicalAgent", run_economical_agent)
109
  workflow.add_node("PoliticalAgent", run_political_agent)
110
  workflow.add_node("MeteorologicalAgent", run_meteorological_agent)
111
 
 
112
  workflow.add_node("GraphInitiator", orchestrator.graph_initiator)
113
  workflow.add_node("FeedAggregatorAgent", orchestrator.feed_aggregator_agent)
114
  workflow.add_node("DataRefresherAgent", orchestrator.data_refresher_agent)
115
  workflow.add_node("DataRefreshRouter", orchestrator.data_refresh_router)
116
 
 
 
117
  workflow.add_edge(START, "GraphInitiator")
118
 
 
119
  sub_agents = [
120
  "SocialAgent",
121
  "IntelligenceAgent",
 
127
  workflow.add_edge("GraphInitiator", agent)
128
  workflow.add_edge(agent, "FeedAggregatorAgent")
129
 
 
130
  workflow.add_edge("FeedAggregatorAgent", "DataRefresherAgent")
131
  workflow.add_edge("DataRefresherAgent", "DataRefreshRouter")
132
 
 
133
  workflow.add_conditional_edges(
134
  "DataRefreshRouter",
135
  lambda x: x.route if x.route else "END",
 
139
  return workflow.compile()
140
 
141
 
142
+ print("Building Combined Agent Graph...")
 
 
 
143
  llm = GroqLLM().get_llm()
144
  builder = CombinedAgentGraphBuilder(llm)
145
  graph = builder.build_graph()
146
+ print("Combined Graph ready")
src/graphs/dataRetrievalAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/graphs/dataRetrievalAgentGraph.py
3
- COMPLETE - Data Retrieval Agent Graph Builder
4
- Implements orchestrator-worker pattern with parallel execution
5
  """
6
 
7
  from langgraph.graph import StateGraph, START, END
@@ -11,27 +9,16 @@ from src.nodes.dataRetrievalAgentNode import DataRetrievalAgentNode
11
 
12
 
13
  class DataRetrievalAgentGraph(DataRetrievalAgentNode):
14
- """
15
- Builds the Data Retrieval Agent graph with orchestrator-worker pattern.
16
- """
17
-
18
  def __init__(self, llm):
19
  super().__init__(llm)
20
  self.llm = llm
21
 
22
  def prepare_worker_tasks(self, state: DataRetrievalAgentState) -> dict:
23
- """
24
- Prepares task list for parallel worker execution
25
- """
26
  tasks = state.generated_tasks
27
  initial_states = [{"generated_tasks": [task]} for task in tasks]
28
  return {"tasks_for_workers": initial_states}
29
 
30
  def create_worker_graph(self):
31
- """
32
- Creates worker subgraph for parallel execution.
33
- Each worker handles one scraping task.
34
- """
35
  worker_graph_builder = StateGraph(DataRetrievalAgentState)
36
 
37
  worker_graph_builder.add_node("worker_agent", self.worker_agent_node)
@@ -44,9 +31,6 @@ class DataRetrievalAgentGraph(DataRetrievalAgentNode):
44
  return worker_graph_builder.compile()
45
 
46
  def aggregate_results(self, state: DataRetrievalAgentState) -> dict:
47
- """
48
- Aggregates results from parallel worker runs
49
- """
50
  worker_outputs = getattr(state, "worker", [])
51
  new_results = []
52
 
@@ -58,51 +42,35 @@ class DataRetrievalAgentGraph(DataRetrievalAgentNode):
58
  return {"worker_results": new_results, "latest_worker_results": new_results}
59
 
60
  def format_output(self, state: DataRetrievalAgentState) -> dict:
61
- """
62
- CRITICAL ADAPTER: Converts ClassifiedEvents to domain_insights format.
63
- This is how data flows to the parent CombinedAgentState.
64
- """
65
  classified_events = state.classified_buffer
66
  insights = []
67
 
68
  for event in classified_events:
69
- insights.append(
70
- {
71
- "source_event_id": event.event_id,
72
- "domain": event.target_agent, # Routes to correct domain agent
73
- "severity": "medium",
74
- "summary": event.content_summary,
75
- "risk_score": event.confidence_score,
76
- }
77
- )
78
 
79
  print(f"[DATA RETRIEVAL] Formatted {len(insights)} insights for parent graph")
80
-
81
  return {"domain_insights": insights}
82
 
83
  def build_data_retrieval_agent_graph(self):
84
- """
85
- Builds the complete data retrieval graph:
86
- Master -> Workers (parallel) -> Aggregator -> Classifier -> Adapter
87
- """
88
  worker_graph = self.create_worker_graph()
89
-
90
  workflow = StateGraph(DataRetrievalAgentState)
91
 
92
- # Add nodes
93
  workflow.add_node("master_delegator", self.master_agent_node)
94
  workflow.add_node("prepare_worker_tasks", self.prepare_worker_tasks)
95
  workflow.add_node(
96
  "worker",
97
- lambda state: {
98
- "worker": worker_graph.map().invoke(state.tasks_for_workers)
99
- },
100
  )
101
  workflow.add_node("aggregate_results", self.aggregate_results)
102
  workflow.add_node("classifier_agent", self.classifier_agent_node)
103
  workflow.add_node("format_output", self.format_output)
104
 
105
- # Wire edges
106
  workflow.set_entry_point("master_delegator")
107
  workflow.add_edge("master_delegator", "prepare_worker_tasks")
108
  workflow.add_edge("prepare_worker_tasks", "worker")
@@ -114,9 +82,6 @@ class DataRetrievalAgentGraph(DataRetrievalAgentNode):
114
  return workflow.compile()
115
 
116
 
117
- # Module-level compilation for LangGraph
118
- print("--- BUILDING DATA RETRIEVAL AGENT GRAPH ---")
119
  llm = GroqLLM().get_llm()
120
  graph_builder = DataRetrievalAgentGraph(llm)
121
  graph = graph_builder.build_data_retrieval_agent_graph()
122
- print("✓ Data Retrieval Agent Graph compiled successfully")
 
1
  """
2
+ dataRetrievalAgentGraph.py - Data Retrieval Agent Graph Builder
 
 
3
  """
4
 
5
  from langgraph.graph import StateGraph, START, END
 
9
 
10
 
11
  class DataRetrievalAgentGraph(DataRetrievalAgentNode):
 
 
 
 
12
  def __init__(self, llm):
13
  super().__init__(llm)
14
  self.llm = llm
15
 
16
  def prepare_worker_tasks(self, state: DataRetrievalAgentState) -> dict:
 
 
 
17
  tasks = state.generated_tasks
18
  initial_states = [{"generated_tasks": [task]} for task in tasks]
19
  return {"tasks_for_workers": initial_states}
20
 
21
  def create_worker_graph(self):
 
 
 
 
22
  worker_graph_builder = StateGraph(DataRetrievalAgentState)
23
 
24
  worker_graph_builder.add_node("worker_agent", self.worker_agent_node)
 
31
  return worker_graph_builder.compile()
32
 
33
  def aggregate_results(self, state: DataRetrievalAgentState) -> dict:
 
 
 
34
  worker_outputs = getattr(state, "worker", [])
35
  new_results = []
36
 
 
42
  return {"worker_results": new_results, "latest_worker_results": new_results}
43
 
44
  def format_output(self, state: DataRetrievalAgentState) -> dict:
 
 
 
 
45
  classified_events = state.classified_buffer
46
  insights = []
47
 
48
  for event in classified_events:
49
+ insights.append({
50
+ "source_event_id": event.event_id,
51
+ "domain": event.target_agent,
52
+ "severity": "medium",
53
+ "summary": event.content_summary,
54
+ "risk_score": event.confidence_score,
55
+ })
 
 
56
 
57
  print(f"[DATA RETRIEVAL] Formatted {len(insights)} insights for parent graph")
 
58
  return {"domain_insights": insights}
59
 
60
  def build_data_retrieval_agent_graph(self):
 
 
 
 
61
  worker_graph = self.create_worker_graph()
 
62
  workflow = StateGraph(DataRetrievalAgentState)
63
 
 
64
  workflow.add_node("master_delegator", self.master_agent_node)
65
  workflow.add_node("prepare_worker_tasks", self.prepare_worker_tasks)
66
  workflow.add_node(
67
  "worker",
68
+ lambda state: {"worker": worker_graph.map().invoke(state.tasks_for_workers)},
 
 
69
  )
70
  workflow.add_node("aggregate_results", self.aggregate_results)
71
  workflow.add_node("classifier_agent", self.classifier_agent_node)
72
  workflow.add_node("format_output", self.format_output)
73
 
 
74
  workflow.set_entry_point("master_delegator")
75
  workflow.add_edge("master_delegator", "prepare_worker_tasks")
76
  workflow.add_edge("prepare_worker_tasks", "worker")
 
82
  return workflow.compile()
83
 
84
 
 
 
85
  llm = GroqLLM().get_llm()
86
  graph_builder = DataRetrievalAgentGraph(llm)
87
  graph = graph_builder.build_data_retrieval_agent_graph()
 
src/graphs/economicalAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/graphs/economicalAgentGraph.py
3
- MODULAR - Economical Agent Graph with Subgraph Architecture
4
- Three independent modules executed in parallel
5
  """
6
 
7
  import uuid
@@ -12,48 +10,27 @@ from src.llms.groqllm import GroqLLM
12
 
13
 
14
  class EconomicalGraphBuilder:
15
- """
16
- Builds the Economical Agent graph with modular subgraph architecture.
17
-
18
- Architecture:
19
- Module 1: Official Sources (CSE Stock + Economic News)
20
- Module 2: Social Media (National + Sectors + World)
21
- Module 3: Feed Generation (Categorize + LLM + Format)
22
- """
23
-
24
  def __init__(self, llm):
25
  self.llm = llm
26
 
27
  def build_official_sources_subgraph(self, node: EconomicalAgentNode) -> StateGraph:
28
- """
29
- Subgraph 1: Official Sources Collection
30
- Collects CSE stock data and local economic news
31
- """
32
  subgraph = StateGraph(EconomicalAgentState)
33
  subgraph.add_node("collect_official", node.collect_official_sources)
34
  subgraph.set_entry_point("collect_official")
35
  subgraph.add_edge("collect_official", END)
36
-
37
  return subgraph.compile()
38
 
39
  def build_social_media_subgraph(self, node: EconomicalAgentNode) -> StateGraph:
40
- """
41
- Subgraph 2: Social Media Collection
42
- Parallel collection of national, sectoral, and world economic media
43
- """
44
  subgraph = StateGraph(EconomicalAgentState)
45
 
46
- # Add collection nodes
47
  subgraph.add_node("national_social", node.collect_national_social_media)
48
  subgraph.add_node("sectoral_social", node.collect_sectoral_social_media)
49
  subgraph.add_node("world_economy", node.collect_world_economy)
50
 
51
- # Set entry point (will fan out to all three)
52
  subgraph.set_entry_point("national_social")
53
  subgraph.set_entry_point("sectoral_social")
54
  subgraph.set_entry_point("world_economy")
55
 
56
- # All converge to END
57
  subgraph.add_edge("national_social", END)
58
  subgraph.add_edge("sectoral_social", END)
59
  subgraph.add_edge("world_economy", END)
@@ -61,10 +38,6 @@ class EconomicalGraphBuilder:
61
  return subgraph.compile()
62
 
63
  def build_feed_generation_subgraph(self, node: EconomicalAgentNode) -> StateGraph:
64
- """
65
- Subgraph 3: Feed Generation
66
- Sequential: Categorize → LLM Summary → Format Output
67
- """
68
  subgraph = StateGraph(EconomicalAgentState)
69
 
70
  subgraph.add_node("categorize", node.categorize_by_sector)
@@ -79,61 +52,29 @@ class EconomicalGraphBuilder:
79
  return subgraph.compile()
80
 
81
  def build_graph(self):
82
- """
83
- Main graph: Orchestrates 3 module subgraphs
84
-
85
- Flow:
86
- 1. Module 1 (Official) + Module 2 (Social) run in parallel
87
- 2. Wait for both to complete
88
- 3. Module 3 (Feed Generation) processes aggregated results
89
- 4. Module 4 (Feed Aggregator) stores unique posts
90
- """
91
  node = EconomicalAgentNode(self.llm)
92
 
93
- # Build subgraphs
94
  official_subgraph = self.build_official_sources_subgraph(node)
95
  social_subgraph = self.build_social_media_subgraph(node)
96
  feed_subgraph = self.build_feed_generation_subgraph(node)
97
 
98
- # Main graph
99
  main_graph = StateGraph(EconomicalAgentState)
100
 
101
- # Add subgraphs as nodes
102
  main_graph.add_node("official_sources_module", official_subgraph.invoke)
103
  main_graph.add_node("social_media_module", social_subgraph.invoke)
104
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
105
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
106
 
107
- # Set parallel execution
108
  main_graph.set_entry_point("official_sources_module")
109
  main_graph.set_entry_point("social_media_module")
110
 
111
- # Both collection modules flow to feed generation
112
  main_graph.add_edge("official_sources_module", "feed_generation_module")
113
  main_graph.add_edge("social_media_module", "feed_generation_module")
114
-
115
- # Feed generation flows to aggregator
116
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
117
-
118
- # Aggregator is the final step
119
  main_graph.add_edge("feed_aggregator", END)
120
 
121
  return main_graph.compile()
122
 
123
 
124
- # Module-level compilation
125
- print("\n" + "=" * 60)
126
- print("🏗️ BUILDING MODULAR ECONOMICAL AGENT GRAPH")
127
- print("=" * 60)
128
- print("Architecture: 3-Module Hybrid Design")
129
- print(" Module 1: Official Sources (CSE Stock + Economic News)")
130
- print(" Module 2: Social Media (5 platforms × 3 scopes)")
131
- print(" Module 3: Feed Generation (Categorize + LLM + Format)")
132
- print(" Module 4: Feed Aggregator (Neo4j + ChromaDB + CSV)")
133
- print("-" * 60)
134
-
135
  llm = GroqLLM().get_llm()
136
  graph = EconomicalGraphBuilder(llm).build_graph()
137
-
138
- print("✅ Economical Agent Graph compiled successfully")
139
- print("=" * 60 + "\n")
 
1
  """
2
+ economicalAgentGraph.py - Economical Agent Graph with Subgraph Architecture
 
 
3
  """
4
 
5
  import uuid
 
10
 
11
 
12
  class EconomicalGraphBuilder:
 
 
 
 
 
 
 
 
 
13
  def __init__(self, llm):
14
  self.llm = llm
15
 
16
  def build_official_sources_subgraph(self, node: EconomicalAgentNode) -> StateGraph:
 
 
 
 
17
  subgraph = StateGraph(EconomicalAgentState)
18
  subgraph.add_node("collect_official", node.collect_official_sources)
19
  subgraph.set_entry_point("collect_official")
20
  subgraph.add_edge("collect_official", END)
 
21
  return subgraph.compile()
22
 
23
  def build_social_media_subgraph(self, node: EconomicalAgentNode) -> StateGraph:
 
 
 
 
24
  subgraph = StateGraph(EconomicalAgentState)
25
 
 
26
  subgraph.add_node("national_social", node.collect_national_social_media)
27
  subgraph.add_node("sectoral_social", node.collect_sectoral_social_media)
28
  subgraph.add_node("world_economy", node.collect_world_economy)
29
 
 
30
  subgraph.set_entry_point("national_social")
31
  subgraph.set_entry_point("sectoral_social")
32
  subgraph.set_entry_point("world_economy")
33
 
 
34
  subgraph.add_edge("national_social", END)
35
  subgraph.add_edge("sectoral_social", END)
36
  subgraph.add_edge("world_economy", END)
 
38
  return subgraph.compile()
39
 
40
  def build_feed_generation_subgraph(self, node: EconomicalAgentNode) -> StateGraph:
 
 
 
 
41
  subgraph = StateGraph(EconomicalAgentState)
42
 
43
  subgraph.add_node("categorize", node.categorize_by_sector)
 
52
  return subgraph.compile()
53
 
54
  def build_graph(self):
 
 
 
 
 
 
 
 
 
55
  node = EconomicalAgentNode(self.llm)
56
 
 
57
  official_subgraph = self.build_official_sources_subgraph(node)
58
  social_subgraph = self.build_social_media_subgraph(node)
59
  feed_subgraph = self.build_feed_generation_subgraph(node)
60
 
 
61
  main_graph = StateGraph(EconomicalAgentState)
62
 
 
63
  main_graph.add_node("official_sources_module", official_subgraph.invoke)
64
  main_graph.add_node("social_media_module", social_subgraph.invoke)
65
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
66
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
67
 
 
68
  main_graph.set_entry_point("official_sources_module")
69
  main_graph.set_entry_point("social_media_module")
70
 
 
71
  main_graph.add_edge("official_sources_module", "feed_generation_module")
72
  main_graph.add_edge("social_media_module", "feed_generation_module")
 
 
73
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
 
 
74
  main_graph.add_edge("feed_aggregator", END)
75
 
76
  return main_graph.compile()
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
79
  llm = GroqLLM().get_llm()
80
  graph = EconomicalGraphBuilder(llm).build_graph()
 
 
 
src/graphs/intelligenceAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/graphs/intelligenceAgentGraph.py
3
- MODULAR - Intelligence Agent Graph with Subgraph Architecture
4
- Three independent modules executed in hybrid parallel/sequential pattern
5
  """
6
 
7
  import uuid
@@ -12,52 +10,27 @@ from src.llms.groqllm import GroqLLM
12
 
13
 
14
  class IntelligenceGraphBuilder:
15
- """
16
- Builds the Intelligence Agent graph with modular subgraph architecture.
17
-
18
- Architecture:
19
- Module 1: Profile Monitoring (Twitter, Facebook, LinkedIn profiles)
20
- Module 2: Competitive Intelligence (Competitor mentions, Product reviews, Market intel)
21
- Module 3: Feed Generation (Categorize + LLM + Format)
22
- """
23
-
24
  def __init__(self, llm):
25
  self.llm = llm
26
 
27
- def build_profile_monitoring_subgraph(
28
- self, node: IntelligenceAgentNode
29
- ) -> StateGraph:
30
- """
31
- Subgraph 1: Profile Monitoring
32
- Monitors competitor social media profiles
33
- """
34
  subgraph = StateGraph(IntelligenceAgentState)
35
  subgraph.add_node("monitor_profiles", node.collect_profile_activity)
36
  subgraph.set_entry_point("monitor_profiles")
37
  subgraph.add_edge("monitor_profiles", END)
38
-
39
  return subgraph.compile()
40
 
41
- def build_competitive_intelligence_subgraph(
42
- self, node: IntelligenceAgentNode
43
- ) -> StateGraph:
44
- """
45
- Subgraph 2: Competitive Intelligence Collection
46
- Parallel collection of competitor mentions, product reviews, market intelligence
47
- """
48
  subgraph = StateGraph(IntelligenceAgentState)
49
 
50
- # Add collection nodes
51
  subgraph.add_node("competitor_mentions", node.collect_competitor_mentions)
52
  subgraph.add_node("product_reviews", node.collect_product_reviews)
53
  subgraph.add_node("market_intelligence", node.collect_market_intelligence)
54
 
55
- # Set parallel entry points
56
  subgraph.set_entry_point("competitor_mentions")
57
  subgraph.set_entry_point("product_reviews")
58
  subgraph.set_entry_point("market_intelligence")
59
 
60
- # All converge to END
61
  subgraph.add_edge("competitor_mentions", END)
62
  subgraph.add_edge("product_reviews", END)
63
  subgraph.add_edge("market_intelligence", END)
@@ -65,10 +38,6 @@ class IntelligenceGraphBuilder:
65
  return subgraph.compile()
66
 
67
  def build_feed_generation_subgraph(self, node: IntelligenceAgentNode) -> StateGraph:
68
- """
69
- Subgraph 3: Feed Generation
70
- Sequential: Categorize -> LLM Summary -> Format Output
71
- """
72
  subgraph = StateGraph(IntelligenceAgentState)
73
 
74
  subgraph.add_node("categorize", node.categorize_intelligence)
@@ -83,63 +52,29 @@ class IntelligenceGraphBuilder:
83
  return subgraph.compile()
84
 
85
  def build_graph(self):
86
- """
87
- Main graph: Orchestrates 3 module subgraphs
88
-
89
- Flow:
90
- 1. Module 1 (Profiles) + Module 2 (Intelligence) run in parallel
91
- 2. Wait for both to complete
92
- 3. Module 3 (Feed Generation) processes aggregated results
93
- 4. Module 4 (Feed Aggregator) stores unique posts
94
- """
95
  node = IntelligenceAgentNode(self.llm)
96
 
97
- # Build subgraphs
98
  profile_subgraph = self.build_profile_monitoring_subgraph(node)
99
  intelligence_subgraph = self.build_competitive_intelligence_subgraph(node)
100
  feed_subgraph = self.build_feed_generation_subgraph(node)
101
 
102
- # Main graph
103
  main_graph = StateGraph(IntelligenceAgentState)
104
 
105
- # Add subgraphs as nodes
106
  main_graph.add_node("profile_monitoring_module", profile_subgraph.invoke)
107
- main_graph.add_node(
108
- "competitive_intelligence_module", intelligence_subgraph.invoke
109
- )
110
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
111
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
112
 
113
- # Set parallel execution
114
  main_graph.set_entry_point("profile_monitoring_module")
115
  main_graph.set_entry_point("competitive_intelligence_module")
116
 
117
- # Both collection modules flow to feed generation
118
  main_graph.add_edge("profile_monitoring_module", "feed_generation_module")
119
  main_graph.add_edge("competitive_intelligence_module", "feed_generation_module")
120
-
121
- # Feed generation flows to aggregator
122
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
123
-
124
- # Aggregator is the final step
125
  main_graph.add_edge("feed_aggregator", END)
126
 
127
  return main_graph.compile()
128
 
129
 
130
- # Module-level compilation
131
- print("\n" + "=" * 60)
132
- print("🏗️ BUILDING MODULAR INTELLIGENCE AGENT GRAPH")
133
- print("=" * 60)
134
- print("Architecture: 3-Module Competitive Intelligence Design")
135
- print(" Module 1: Profile Monitoring (Twitter, Facebook, LinkedIn)")
136
- print(" Module 2: Competitive Intelligence (Mentions, Reviews, Market)")
137
- print(" Module 3: Feed Generation (Categorize + LLM + Format)")
138
- print(" Module 4: Feed Aggregator (Neo4j + ChromaDB + CSV)")
139
- print("-" * 60)
140
-
141
  llm = GroqLLM().get_llm()
142
  graph = IntelligenceGraphBuilder(llm).build_graph()
143
-
144
- print("✅ Intelligence Agent Graph compiled successfully")
145
- print("=" * 60 + "\n")
 
1
  """
2
+ intelligenceAgentGraph.py - Intelligence Agent Graph with Subgraph Architecture
 
 
3
  """
4
 
5
  import uuid
 
10
 
11
 
12
  class IntelligenceGraphBuilder:
 
 
 
 
 
 
 
 
 
13
  def __init__(self, llm):
14
  self.llm = llm
15
 
16
+ def build_profile_monitoring_subgraph(self, node: IntelligenceAgentNode) -> StateGraph:
 
 
 
 
 
 
17
  subgraph = StateGraph(IntelligenceAgentState)
18
  subgraph.add_node("monitor_profiles", node.collect_profile_activity)
19
  subgraph.set_entry_point("monitor_profiles")
20
  subgraph.add_edge("monitor_profiles", END)
 
21
  return subgraph.compile()
22
 
23
+ def build_competitive_intelligence_subgraph(self, node: IntelligenceAgentNode) -> StateGraph:
 
 
 
 
 
 
24
  subgraph = StateGraph(IntelligenceAgentState)
25
 
 
26
  subgraph.add_node("competitor_mentions", node.collect_competitor_mentions)
27
  subgraph.add_node("product_reviews", node.collect_product_reviews)
28
  subgraph.add_node("market_intelligence", node.collect_market_intelligence)
29
 
 
30
  subgraph.set_entry_point("competitor_mentions")
31
  subgraph.set_entry_point("product_reviews")
32
  subgraph.set_entry_point("market_intelligence")
33
 
 
34
  subgraph.add_edge("competitor_mentions", END)
35
  subgraph.add_edge("product_reviews", END)
36
  subgraph.add_edge("market_intelligence", END)
 
38
  return subgraph.compile()
39
 
40
  def build_feed_generation_subgraph(self, node: IntelligenceAgentNode) -> StateGraph:
 
 
 
 
41
  subgraph = StateGraph(IntelligenceAgentState)
42
 
43
  subgraph.add_node("categorize", node.categorize_intelligence)
 
52
  return subgraph.compile()
53
 
54
  def build_graph(self):
 
 
 
 
 
 
 
 
 
55
  node = IntelligenceAgentNode(self.llm)
56
 
 
57
  profile_subgraph = self.build_profile_monitoring_subgraph(node)
58
  intelligence_subgraph = self.build_competitive_intelligence_subgraph(node)
59
  feed_subgraph = self.build_feed_generation_subgraph(node)
60
 
 
61
  main_graph = StateGraph(IntelligenceAgentState)
62
 
 
63
  main_graph.add_node("profile_monitoring_module", profile_subgraph.invoke)
64
+ main_graph.add_node("competitive_intelligence_module", intelligence_subgraph.invoke)
 
 
65
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
66
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
67
 
 
68
  main_graph.set_entry_point("profile_monitoring_module")
69
  main_graph.set_entry_point("competitive_intelligence_module")
70
 
 
71
  main_graph.add_edge("profile_monitoring_module", "feed_generation_module")
72
  main_graph.add_edge("competitive_intelligence_module", "feed_generation_module")
 
 
73
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
 
 
74
  main_graph.add_edge("feed_aggregator", END)
75
 
76
  return main_graph.compile()
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
79
  llm = GroqLLM().get_llm()
80
  graph = IntelligenceGraphBuilder(llm).build_graph()
 
 
 
src/graphs/meteorologicalAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/graphs/meteorologicalAgentGraph.py
3
- MODULAR - Meteorological Agent Graph with Subgraph Architecture
4
- Three independent modules executed in parallel
5
  """
6
 
7
  import uuid
@@ -12,63 +10,34 @@ from src.llms.groqllm import GroqLLM
12
 
13
 
14
  class MeteorologicalGraphBuilder:
15
- """
16
- Builds the Meteorological Agent graph with modular subgraph architecture.
17
-
18
- Architecture:
19
- Module 1: Official Weather Sources (DMC + Weather Nowcast)
20
- Module 2: Social Media (National + Districts + Climate)
21
- Module 3: Feed Generation (Categorize + LLM + Format)
22
- """
23
-
24
  def __init__(self, llm):
25
  self.llm = llm
26
 
27
- def build_official_sources_subgraph(
28
- self, node: MeteorologicalAgentNode
29
- ) -> StateGraph:
30
- """
31
- Subgraph 1: Official Weather Sources Collection
32
- Collects DMC alerts and weather nowcast data
33
- """
34
  subgraph = StateGraph(MeteorologicalAgentState)
35
  subgraph.add_node("collect_official", node.collect_official_sources)
36
  subgraph.set_entry_point("collect_official")
37
  subgraph.add_edge("collect_official", END)
38
-
39
  return subgraph.compile()
40
 
41
  def build_social_media_subgraph(self, node: MeteorologicalAgentNode) -> StateGraph:
42
- """
43
- Subgraph 2: Social Media Collection
44
- Parallel collection of national, district, and climate weather media
45
- """
46
  subgraph = StateGraph(MeteorologicalAgentState)
47
 
48
- # Add collection nodes
49
  subgraph.add_node("national_social", node.collect_national_social_media)
50
  subgraph.add_node("district_social", node.collect_district_social_media)
51
  subgraph.add_node("climate_alerts", node.collect_climate_alerts)
52
 
53
- # Set entry point (will fan out to all three)
54
  subgraph.set_entry_point("national_social")
55
  subgraph.set_entry_point("district_social")
56
  subgraph.set_entry_point("climate_alerts")
57
 
58
- # All converge to END
59
  subgraph.add_edge("national_social", END)
60
  subgraph.add_edge("district_social", END)
61
  subgraph.add_edge("climate_alerts", END)
62
 
63
  return subgraph.compile()
64
 
65
- def build_feed_generation_subgraph(
66
- self, node: MeteorologicalAgentNode
67
- ) -> StateGraph:
68
- """
69
- Subgraph 3: Feed Generation
70
- Sequential: Categorize → LLM Summary → Format Output
71
- """
72
  subgraph = StateGraph(MeteorologicalAgentState)
73
 
74
  subgraph.add_node("categorize", node.categorize_by_geography)
@@ -83,61 +52,29 @@ class MeteorologicalGraphBuilder:
83
  return subgraph.compile()
84
 
85
  def build_graph(self):
86
- """
87
- Main graph: Orchestrates 3 module subgraphs
88
-
89
- Flow:
90
- 1. Module 1 (Official) + Module 2 (Social) run in parallel
91
- 2. Wait for both to complete
92
- 3. Module 3 (Feed Generation) processes aggregated results
93
- 4. Module 4 (Feed Aggregator) stores unique posts
94
- """
95
  node = MeteorologicalAgentNode(self.llm)
96
 
97
- # Build subgraphs
98
  official_subgraph = self.build_official_sources_subgraph(node)
99
  social_subgraph = self.build_social_media_subgraph(node)
100
  feed_subgraph = self.build_feed_generation_subgraph(node)
101
 
102
- # Main graph
103
  main_graph = StateGraph(MeteorologicalAgentState)
104
 
105
- # Add subgraphs as nodes
106
  main_graph.add_node("official_sources_module", official_subgraph.invoke)
107
  main_graph.add_node("social_media_module", social_subgraph.invoke)
108
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
109
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
110
 
111
- # Set parallel execution
112
  main_graph.set_entry_point("official_sources_module")
113
  main_graph.set_entry_point("social_media_module")
114
 
115
- # Both collection modules flow to feed generation
116
  main_graph.add_edge("official_sources_module", "feed_generation_module")
117
  main_graph.add_edge("social_media_module", "feed_generation_module")
118
-
119
- # Feed generation flows to aggregator
120
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
121
-
122
- # Aggregator is the final step
123
  main_graph.add_edge("feed_aggregator", END)
124
 
125
  return main_graph.compile()
126
 
127
 
128
- # Module-level compilation
129
- print("\n" + "=" * 60)
130
- print("🏗️ BUILDING MODULAR METEOROLOGICAL AGENT GRAPH")
131
- print("=" * 60)
132
- print("Architecture: 3-Module Hybrid Design")
133
- print(" Module 1: Official Sources (DMC Alerts + Weather Nowcast)")
134
- print(" Module 2: Social Media (5 platforms × 3 scopes)")
135
- print(" Module 3: Feed Generation (Categorize + LLM + Format)")
136
- print(" Module 4: Feed Aggregator (Neo4j + ChromaDB + CSV)")
137
- print("-" * 60)
138
-
139
  llm = GroqLLM().get_llm()
140
  graph = MeteorologicalGraphBuilder(llm).build_graph()
141
-
142
- print("✅ Meteorological Agent Graph compiled successfully")
143
- print("=" * 60 + "\n")
 
1
  """
2
+ meteorologicalAgentGraph.py - Meteorological Agent Graph with Subgraph Architecture
 
 
3
  """
4
 
5
  import uuid
 
10
 
11
 
12
  class MeteorologicalGraphBuilder:
 
 
 
 
 
 
 
 
 
13
  def __init__(self, llm):
14
  self.llm = llm
15
 
16
+ def build_official_sources_subgraph(self, node: MeteorologicalAgentNode) -> StateGraph:
 
 
 
 
 
 
17
  subgraph = StateGraph(MeteorologicalAgentState)
18
  subgraph.add_node("collect_official", node.collect_official_sources)
19
  subgraph.set_entry_point("collect_official")
20
  subgraph.add_edge("collect_official", END)
 
21
  return subgraph.compile()
22
 
23
  def build_social_media_subgraph(self, node: MeteorologicalAgentNode) -> StateGraph:
 
 
 
 
24
  subgraph = StateGraph(MeteorologicalAgentState)
25
 
 
26
  subgraph.add_node("national_social", node.collect_national_social_media)
27
  subgraph.add_node("district_social", node.collect_district_social_media)
28
  subgraph.add_node("climate_alerts", node.collect_climate_alerts)
29
 
 
30
  subgraph.set_entry_point("national_social")
31
  subgraph.set_entry_point("district_social")
32
  subgraph.set_entry_point("climate_alerts")
33
 
 
34
  subgraph.add_edge("national_social", END)
35
  subgraph.add_edge("district_social", END)
36
  subgraph.add_edge("climate_alerts", END)
37
 
38
  return subgraph.compile()
39
 
40
+ def build_feed_generation_subgraph(self, node: MeteorologicalAgentNode) -> StateGraph:
 
 
 
 
 
 
41
  subgraph = StateGraph(MeteorologicalAgentState)
42
 
43
  subgraph.add_node("categorize", node.categorize_by_geography)
 
52
  return subgraph.compile()
53
 
54
  def build_graph(self):
 
 
 
 
 
 
 
 
 
55
  node = MeteorologicalAgentNode(self.llm)
56
 
 
57
  official_subgraph = self.build_official_sources_subgraph(node)
58
  social_subgraph = self.build_social_media_subgraph(node)
59
  feed_subgraph = self.build_feed_generation_subgraph(node)
60
 
 
61
  main_graph = StateGraph(MeteorologicalAgentState)
62
 
 
63
  main_graph.add_node("official_sources_module", official_subgraph.invoke)
64
  main_graph.add_node("social_media_module", social_subgraph.invoke)
65
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
66
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
67
 
 
68
  main_graph.set_entry_point("official_sources_module")
69
  main_graph.set_entry_point("social_media_module")
70
 
 
71
  main_graph.add_edge("official_sources_module", "feed_generation_module")
72
  main_graph.add_edge("social_media_module", "feed_generation_module")
 
 
73
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
 
 
74
  main_graph.add_edge("feed_aggregator", END)
75
 
76
  return main_graph.compile()
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
79
  llm = GroqLLM().get_llm()
80
  graph = MeteorologicalGraphBuilder(llm).build_graph()
 
 
 
src/graphs/politicalAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/graphs/politicalAgentGraph.py
3
- MODULAR - Political Agent Graph with Subgraph Architecture
4
- Three independent modules executed in parallel
5
  """
6
 
7
  import uuid
@@ -12,48 +10,26 @@ from src.llms.groqllm import GroqLLM
12
 
13
 
14
  class PoliticalGraphBuilder:
15
- """
16
- Builds the Political Agent graph with modular subgraph architecture.
17
-
18
- Architecture:
19
- Module 1: Official Sources (Gazette + Parliament)
20
- Module 2: Social Media (National + Districts + World)
21
- Module 3: Feed Generation (Categorize + LLM + Format)
22
- """
23
-
24
  def __init__(self, llm):
25
  self.llm = llm
26
 
27
  def build_official_sources_subgraph(self, node: PoliticalAgentNode) -> StateGraph:
28
- """
29
- Subgraph 1: Official Sources Collection
30
- Collects government gazette and parliament minutes
31
- """
32
  subgraph = StateGraph(PoliticalAgentState)
33
  subgraph.add_node("collect_official", node.collect_official_sources)
34
  subgraph.set_entry_point("collect_official")
35
  subgraph.add_edge("collect_official", END)
36
-
37
  return subgraph.compile()
38
 
39
  def build_social_media_subgraph(self, node: PoliticalAgentNode) -> StateGraph:
40
- """
41
- Subgraph 2: Social Media Collection
42
- Parallel collection of national, district, and world social media
43
- """
44
  subgraph = StateGraph(PoliticalAgentState)
45
-
46
- # Add collection nodes
47
  subgraph.add_node("national_social", node.collect_national_social_media)
48
  subgraph.add_node("district_social", node.collect_district_social_media)
49
  subgraph.add_node("world_politics", node.collect_world_politics)
50
 
51
- # Set entry point (will fan out to all three)
52
  subgraph.set_entry_point("national_social")
53
  subgraph.set_entry_point("district_social")
54
  subgraph.set_entry_point("world_politics")
55
 
56
- # All converge to END
57
  subgraph.add_edge("national_social", END)
58
  subgraph.add_edge("district_social", END)
59
  subgraph.add_edge("world_politics", END)
@@ -61,10 +37,6 @@ class PoliticalGraphBuilder:
61
  return subgraph.compile()
62
 
63
  def build_feed_generation_subgraph(self, node: PoliticalAgentNode) -> StateGraph:
64
- """
65
- Subgraph 3: Feed Generation
66
- Sequential: Categorize → LLM Summary → Format Output
67
- """
68
  subgraph = StateGraph(PoliticalAgentState)
69
 
70
  subgraph.add_node("categorize", node.categorize_by_geography)
@@ -79,61 +51,29 @@ class PoliticalGraphBuilder:
79
  return subgraph.compile()
80
 
81
  def build_graph(self):
82
- """
83
- Main graph: Orchestrates 3 module subgraphs
84
-
85
- Flow:
86
- 1. Module 1 (Official) + Module 2 (Social) run in parallel
87
- 2. Wait for both to complete
88
- 3. Module 3 (Feed Generation) processes aggregated results
89
- 4. Module 4 (Feed Aggregator) stores unique posts
90
- """
91
  node = PoliticalAgentNode(self.llm)
92
 
93
- # Build subgraphs
94
  official_subgraph = self.build_official_sources_subgraph(node)
95
  social_subgraph = self.build_social_media_subgraph(node)
96
  feed_subgraph = self.build_feed_generation_subgraph(node)
97
 
98
- # Main graph
99
  main_graph = StateGraph(PoliticalAgentState)
100
 
101
- # Add subgraphs as nodes
102
  main_graph.add_node("official_sources_module", official_subgraph.invoke)
103
  main_graph.add_node("social_media_module", social_subgraph.invoke)
104
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
105
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
106
 
107
- # Set parallel execution
108
  main_graph.set_entry_point("official_sources_module")
109
  main_graph.set_entry_point("social_media_module")
110
 
111
- # Both collection modules flow to feed generation
112
  main_graph.add_edge("official_sources_module", "feed_generation_module")
113
  main_graph.add_edge("social_media_module", "feed_generation_module")
114
-
115
- # Feed generation flows to aggregator
116
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
117
-
118
- # Aggregator is the final step
119
  main_graph.add_edge("feed_aggregator", END)
120
 
121
  return main_graph.compile()
122
 
123
 
124
- # Module-level compilation
125
- print("\n" + "=" * 60)
126
- print("🏗️ BUILDING MODULAR POLITICAL AGENT GRAPH")
127
- print("=" * 60)
128
- print("Architecture: 3-Module Hybrid Design")
129
- print(" Module 1: Official Sources (Gazette + Parliament)")
130
- print(" Module 2: Social Media (5 platforms × 3 scopes)")
131
- print(" Module 3: Feed Generation (Categorize + LLM + Format)")
132
- print(" Module 4: Feed Aggregator (Neo4j + ChromaDB + CSV)")
133
- print("-" * 60)
134
-
135
  llm = GroqLLM().get_llm()
136
  graph = PoliticalGraphBuilder(llm).build_graph()
137
-
138
- print("✅ Political Agent Graph compiled successfully")
139
- print("=" * 60 + "\n")
 
1
  """
2
+ politicalAgentGraph.py - Political Agent Graph with Subgraph Architecture
 
 
3
  """
4
 
5
  import uuid
 
10
 
11
 
12
  class PoliticalGraphBuilder:
 
 
 
 
 
 
 
 
 
13
  def __init__(self, llm):
14
  self.llm = llm
15
 
16
  def build_official_sources_subgraph(self, node: PoliticalAgentNode) -> StateGraph:
 
 
 
 
17
  subgraph = StateGraph(PoliticalAgentState)
18
  subgraph.add_node("collect_official", node.collect_official_sources)
19
  subgraph.set_entry_point("collect_official")
20
  subgraph.add_edge("collect_official", END)
 
21
  return subgraph.compile()
22
 
23
  def build_social_media_subgraph(self, node: PoliticalAgentNode) -> StateGraph:
 
 
 
 
24
  subgraph = StateGraph(PoliticalAgentState)
 
 
25
  subgraph.add_node("national_social", node.collect_national_social_media)
26
  subgraph.add_node("district_social", node.collect_district_social_media)
27
  subgraph.add_node("world_politics", node.collect_world_politics)
28
 
 
29
  subgraph.set_entry_point("national_social")
30
  subgraph.set_entry_point("district_social")
31
  subgraph.set_entry_point("world_politics")
32
 
 
33
  subgraph.add_edge("national_social", END)
34
  subgraph.add_edge("district_social", END)
35
  subgraph.add_edge("world_politics", END)
 
37
  return subgraph.compile()
38
 
39
  def build_feed_generation_subgraph(self, node: PoliticalAgentNode) -> StateGraph:
 
 
 
 
40
  subgraph = StateGraph(PoliticalAgentState)
41
 
42
  subgraph.add_node("categorize", node.categorize_by_geography)
 
51
  return subgraph.compile()
52
 
53
  def build_graph(self):
 
 
 
 
 
 
 
 
 
54
  node = PoliticalAgentNode(self.llm)
55
 
 
56
  official_subgraph = self.build_official_sources_subgraph(node)
57
  social_subgraph = self.build_social_media_subgraph(node)
58
  feed_subgraph = self.build_feed_generation_subgraph(node)
59
 
 
60
  main_graph = StateGraph(PoliticalAgentState)
61
 
 
62
  main_graph.add_node("official_sources_module", official_subgraph.invoke)
63
  main_graph.add_node("social_media_module", social_subgraph.invoke)
64
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
65
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
66
 
 
67
  main_graph.set_entry_point("official_sources_module")
68
  main_graph.set_entry_point("social_media_module")
69
 
 
70
  main_graph.add_edge("official_sources_module", "feed_generation_module")
71
  main_graph.add_edge("social_media_module", "feed_generation_module")
 
 
72
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
 
 
73
  main_graph.add_edge("feed_aggregator", END)
74
 
75
  return main_graph.compile()
76
 
77
 
 
 
 
 
 
 
 
 
 
 
 
78
  llm = GroqLLM().get_llm()
79
  graph = PoliticalGraphBuilder(llm).build_graph()
 
 
 
src/graphs/socialAgentGraph.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/graphs/socialAgentGraph.py
3
- MODULAR - Social Agent Graph with Subgraph Architecture
4
- Three independent modules for social intelligence collection
5
  """
6
 
7
  import uuid
@@ -12,48 +10,27 @@ from src.llms.groqllm import GroqLLM
12
 
13
 
14
  class SocialGraphBuilder:
15
- """
16
- Builds the Social Agent graph with modular subgraph architecture.
17
-
18
- Architecture:
19
- Module 1: Trending Topics (Sri Lanka specific)
20
- Module 2: Social Media (Sri Lanka + Asia + World)
21
- Module 3: Feed Generation (Categorize + LLM + Format)
22
- """
23
-
24
  def __init__(self, llm):
25
  self.llm = llm
26
 
27
  def build_trending_subgraph(self, node: SocialAgentNode) -> StateGraph:
28
- """
29
- Subgraph 1: Trending Topics Collection
30
- Collects Sri Lankan trending topics
31
- """
32
  subgraph = StateGraph(SocialAgentState)
33
  subgraph.add_node("collect_trends", node.collect_sri_lanka_trends)
34
  subgraph.set_entry_point("collect_trends")
35
  subgraph.add_edge("collect_trends", END)
36
-
37
  return subgraph.compile()
38
 
39
  def build_social_media_subgraph(self, node: SocialAgentNode) -> StateGraph:
40
- """
41
- Subgraph 2: Social Media Collection
42
- Parallel collection across three geographic scopes
43
- """
44
  subgraph = StateGraph(SocialAgentState)
45
 
46
- # Add collection nodes
47
  subgraph.add_node("sri_lanka_social", node.collect_sri_lanka_social_media)
48
  subgraph.add_node("asia_social", node.collect_asia_social_media)
49
  subgraph.add_node("world_social", node.collect_world_social_media)
50
 
51
- # Set entry point (will fan out to all three)
52
  subgraph.set_entry_point("sri_lanka_social")
53
  subgraph.set_entry_point("asia_social")
54
  subgraph.set_entry_point("world_social")
55
 
56
- # All converge to END
57
  subgraph.add_edge("sri_lanka_social", END)
58
  subgraph.add_edge("asia_social", END)
59
  subgraph.add_edge("world_social", END)
@@ -61,10 +38,6 @@ class SocialGraphBuilder:
61
  return subgraph.compile()
62
 
63
  def build_feed_generation_subgraph(self, node: SocialAgentNode) -> StateGraph:
64
- """
65
- Subgraph 3: Feed Generation
66
- Sequential: Categorize → LLM Summary → Format Output
67
- """
68
  subgraph = StateGraph(SocialAgentState)
69
 
70
  subgraph.add_node("categorize", node.categorize_by_geography)
@@ -79,61 +52,29 @@ class SocialGraphBuilder:
79
  return subgraph.compile()
80
 
81
  def build_graph(self):
82
- """
83
- Main graph: Orchestrates 3 module subgraphs
84
-
85
- Flow:
86
- 1. Module 1 (Trending) + Module 2 (Social) run in parallel
87
- 2. Wait for both to complete
88
- 3. Module 3 (Feed Generation) processes aggregated results
89
- 4. Module 4 (Feed Aggregator) stores unique posts
90
- """
91
  node = SocialAgentNode(self.llm)
92
 
93
- # Build subgraphs
94
  trending_subgraph = self.build_trending_subgraph(node)
95
  social_subgraph = self.build_social_media_subgraph(node)
96
  feed_subgraph = self.build_feed_generation_subgraph(node)
97
 
98
- # Main graph
99
  main_graph = StateGraph(SocialAgentState)
100
 
101
- # Add subgraphs as nodes
102
  main_graph.add_node("trending_module", trending_subgraph.invoke)
103
  main_graph.add_node("social_media_module", social_subgraph.invoke)
104
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
105
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
106
 
107
- # Set parallel execution
108
  main_graph.set_entry_point("trending_module")
109
  main_graph.set_entry_point("social_media_module")
110
 
111
- # Both collection modules flow to feed generation
112
  main_graph.add_edge("trending_module", "feed_generation_module")
113
  main_graph.add_edge("social_media_module", "feed_generation_module")
114
-
115
- # Feed generation flows to aggregator
116
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
117
-
118
- # Aggregator is the final step
119
  main_graph.add_edge("feed_aggregator", END)
120
 
121
  return main_graph.compile()
122
 
123
 
124
- # Module-level compilation
125
- print("\n" + "=" * 60)
126
- print("[BUILD] MODULAR SOCIAL AGENT GRAPH")
127
- print("=" * 60)
128
- print("Architecture: 3-Module Hybrid Design")
129
- print(" Module 1: Trending Topics (Sri Lanka specific)")
130
- print(" Module 2: Social Media (5 platforms × 3 geographic scopes)")
131
- print(" Module 3: Feed Generation (Categorize + LLM + Format)")
132
- print(" Module 4: Feed Aggregator (Neo4j + ChromaDB + CSV)")
133
- print("-" * 60)
134
-
135
  llm = GroqLLM().get_llm()
136
  graph = SocialGraphBuilder(llm).build_graph()
137
-
138
- print("[OK] Social Agent Graph compiled successfully")
139
- print("=" * 60 + "\n")
 
1
  """
2
+ socialAgentGraph.py - Social Agent Graph with Subgraph Architecture
 
 
3
  """
4
 
5
  import uuid
 
10
 
11
 
12
  class SocialGraphBuilder:
 
 
 
 
 
 
 
 
 
13
  def __init__(self, llm):
14
  self.llm = llm
15
 
16
  def build_trending_subgraph(self, node: SocialAgentNode) -> StateGraph:
 
 
 
 
17
  subgraph = StateGraph(SocialAgentState)
18
  subgraph.add_node("collect_trends", node.collect_sri_lanka_trends)
19
  subgraph.set_entry_point("collect_trends")
20
  subgraph.add_edge("collect_trends", END)
 
21
  return subgraph.compile()
22
 
23
  def build_social_media_subgraph(self, node: SocialAgentNode) -> StateGraph:
 
 
 
 
24
  subgraph = StateGraph(SocialAgentState)
25
 
 
26
  subgraph.add_node("sri_lanka_social", node.collect_sri_lanka_social_media)
27
  subgraph.add_node("asia_social", node.collect_asia_social_media)
28
  subgraph.add_node("world_social", node.collect_world_social_media)
29
 
 
30
  subgraph.set_entry_point("sri_lanka_social")
31
  subgraph.set_entry_point("asia_social")
32
  subgraph.set_entry_point("world_social")
33
 
 
34
  subgraph.add_edge("sri_lanka_social", END)
35
  subgraph.add_edge("asia_social", END)
36
  subgraph.add_edge("world_social", END)
 
38
  return subgraph.compile()
39
 
40
  def build_feed_generation_subgraph(self, node: SocialAgentNode) -> StateGraph:
 
 
 
 
41
  subgraph = StateGraph(SocialAgentState)
42
 
43
  subgraph.add_node("categorize", node.categorize_by_geography)
 
52
  return subgraph.compile()
53
 
54
  def build_graph(self):
 
 
 
 
 
 
 
 
 
55
  node = SocialAgentNode(self.llm)
56
 
 
57
  trending_subgraph = self.build_trending_subgraph(node)
58
  social_subgraph = self.build_social_media_subgraph(node)
59
  feed_subgraph = self.build_feed_generation_subgraph(node)
60
 
 
61
  main_graph = StateGraph(SocialAgentState)
62
 
 
63
  main_graph.add_node("trending_module", trending_subgraph.invoke)
64
  main_graph.add_node("social_media_module", social_subgraph.invoke)
65
  main_graph.add_node("feed_generation_module", feed_subgraph.invoke)
66
  main_graph.add_node("feed_aggregator", node.aggregate_and_store_feeds)
67
 
 
68
  main_graph.set_entry_point("trending_module")
69
  main_graph.set_entry_point("social_media_module")
70
 
 
71
  main_graph.add_edge("trending_module", "feed_generation_module")
72
  main_graph.add_edge("social_media_module", "feed_generation_module")
 
 
73
  main_graph.add_edge("feed_generation_module", "feed_aggregator")
 
 
74
  main_graph.add_edge("feed_aggregator", END)
75
 
76
  return main_graph.compile()
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
79
  llm = GroqLLM().get_llm()
80
  graph = SocialGraphBuilder(llm).build_graph()
 
 
 
src/graphs/vectorizationAgentGraph.py CHANGED
@@ -1,6 +1,5 @@
1
  """
2
- src/graphs/vectorizationAgentGraph.py
3
- Vectorization Agent Graph - Agentic workflow for text-to-vector conversion
4
  """
5
 
6
  from langgraph.graph import StateGraph, END
@@ -10,34 +9,14 @@ from src.llms.groqllm import GroqLLM
10
 
11
 
12
  class VectorizationGraphBuilder:
13
- """
14
- Builds the Vectorization Agent graph.
15
-
16
- Architecture (Sequential Pipeline):
17
- Step 1: Language Detection (FastText/lingua-py)
18
- Step 2: Text Vectorization (SinhalaBERTo/Tamil-BERT/DistilBERT)
19
- Step 3: Anomaly Detection (Isolation Forest on vectors)
20
- Step 4: Trending Detection (Velocity/Spike tracking)
21
- Step 5: Expert Summary (GroqLLM)
22
- Step 6: Format Output
23
- """
24
-
25
  def __init__(self, llm=None):
26
  self.llm = llm or GroqLLM().get_llm()
27
 
28
  def build_graph(self):
29
- """
30
- Build the vectorization agent graph.
31
-
32
- Flow:
33
- detect_languages → vectorize_texts → anomaly_detection → trending_detection → expert_summary → format_output → END
34
- """
35
  node = VectorizationAgentNode(self.llm)
36
 
37
- # Create graph
38
  graph = StateGraph(VectorizationAgentState)
39
 
40
- # Add nodes
41
  graph.add_node("detect_languages", node.detect_languages)
42
  graph.add_node("vectorize_texts", node.vectorize_texts)
43
  graph.add_node("anomaly_detection", node.run_anomaly_detection)
@@ -45,10 +24,8 @@ class VectorizationGraphBuilder:
45
  graph.add_node("generate_expert_summary", node.generate_expert_summary)
46
  graph.add_node("format_output", node.format_final_output)
47
 
48
- # Set entry point
49
  graph.set_entry_point("detect_languages")
50
 
51
- # Sequential flow with anomaly + trending detection
52
  graph.add_edge("detect_languages", "vectorize_texts")
53
  graph.add_edge("vectorize_texts", "anomaly_detection")
54
  graph.add_edge("anomaly_detection", "trending_detection")
@@ -59,21 +36,5 @@ class VectorizationGraphBuilder:
59
  return graph.compile()
60
 
61
 
62
- # Module-level compilation
63
- print("\n" + "=" * 60)
64
- print("[BRAIN] BUILDING VECTORIZATION AGENT GRAPH")
65
- print("=" * 60)
66
- print("Architecture: 6-Step Sequential Pipeline")
67
- print(" Step 1: Language Detection (FastText/Unicode)")
68
- print(" Step 2: Text Vectorization (SinhalaBERTo/Tamil-BERT/DistilBERT)")
69
- print(" Step 3: Anomaly Detection (Isolation Forest)")
70
- print(" Step 4: Trending Detection (Velocity/Spikes)")
71
- print(" Step 5: Expert Summary (GroqLLM)")
72
- print(" Step 6: Format Output")
73
- print("-" * 60)
74
-
75
  llm = GroqLLM().get_llm()
76
  graph = VectorizationGraphBuilder(llm).build_graph()
77
-
78
- print("[OK] Vectorization Agent Graph compiled successfully")
79
- print("=" * 60 + "\n")
 
1
  """
2
+ vectorizationAgentGraph.py - Vectorization Agent Graph for text-to-vector conversion
 
3
  """
4
 
5
  from langgraph.graph import StateGraph, END
 
9
 
10
 
11
  class VectorizationGraphBuilder:
 
 
 
 
 
 
 
 
 
 
 
 
12
  def __init__(self, llm=None):
13
  self.llm = llm or GroqLLM().get_llm()
14
 
15
  def build_graph(self):
 
 
 
 
 
 
16
  node = VectorizationAgentNode(self.llm)
17
 
 
18
  graph = StateGraph(VectorizationAgentState)
19
 
 
20
  graph.add_node("detect_languages", node.detect_languages)
21
  graph.add_node("vectorize_texts", node.vectorize_texts)
22
  graph.add_node("anomaly_detection", node.run_anomaly_detection)
 
24
  graph.add_node("generate_expert_summary", node.generate_expert_summary)
25
  graph.add_node("format_output", node.format_final_output)
26
 
 
27
  graph.set_entry_point("detect_languages")
28
 
 
29
  graph.add_edge("detect_languages", "vectorize_texts")
30
  graph.add_edge("vectorize_texts", "anomaly_detection")
31
  graph.add_edge("anomaly_detection", "trending_detection")
 
36
  return graph.compile()
37
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  llm = GroqLLM().get_llm()
40
  graph = VectorizationGraphBuilder(llm).build_graph()
 
 
 
src/rag.py CHANGED
@@ -1,7 +1,5 @@
1
  """
2
- src/rag.py
3
- Chat-History Aware RAG Application for Roger Intelligence Platform
4
- Connects to all ChromaDB collections used by the agent graph for conversational Q&A.
5
  """
6
 
7
  import os
@@ -11,14 +9,11 @@ from typing import List, Dict, Any, Optional, Tuple
11
  from datetime import datetime
12
  import logging
13
 
14
- # Add project root to path
15
  PROJECT_ROOT = Path(__file__).parent.parent
16
  sys.path.insert(0, str(PROJECT_ROOT))
17
 
18
- # Load environment variables
19
  try:
20
  from dotenv import load_dotenv
21
-
22
  load_dotenv()
23
  except ImportError:
24
  pass
@@ -28,18 +23,13 @@ logging.basicConfig(
28
  level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
29
  )
30
 
31
- # ============================================
32
- # IMPORTS
33
- # ============================================
34
-
35
  try:
36
  import chromadb
37
  from chromadb.config import Settings
38
-
39
  CHROMA_AVAILABLE = True
40
  except ImportError:
41
  CHROMA_AVAILABLE = False
42
- logger.warning("[RAG] ChromaDB not available. Install with: pip install chromadb")
43
 
44
  try:
45
  from langchain_groq import ChatGroq
@@ -47,31 +37,14 @@ try:
47
  from langchain_core.messages import HumanMessage, AIMessage
48
  from langchain_core.output_parsers import StrOutputParser
49
  from langchain_core.runnables import RunnablePassthrough
50
-
51
  LANGCHAIN_AVAILABLE = True
52
  except ImportError:
53
  LANGCHAIN_AVAILABLE = False
54
- logger.warning(
55
- "[RAG] LangChain not available. Install with: pip install langchain-groq langchain-core"
56
- )
57
-
58
-
59
- # ============================================
60
- # CHROMADB MULTI-COLLECTION RETRIEVER
61
- # ============================================
62
 
63
 
64
  class MultiCollectionRetriever:
65
- """
66
- Connects to all ChromaDB collections used by Roger agents.
67
- Provides unified search across all intelligence data.
68
- """
69
-
70
- # Known collections from the agents
71
- COLLECTIONS = [
72
- "Roger_feeds", # From chromadb_store.py (storage manager)
73
- "Roger_rag_collection", # From db_manager.py (agent nodes)
74
- ]
75
 
76
  def __init__(self, persist_directory: str = None):
77
  self.persist_directory = persist_directory or os.getenv(
@@ -81,45 +54,37 @@ class MultiCollectionRetriever:
81
  self.collections: Dict[str, Any] = {}
82
 
83
  if not CHROMA_AVAILABLE:
84
- logger.error("[RAG] ChromaDB not installed!")
85
  return
86
 
87
  self._init_client()
88
 
89
  def _init_client(self):
90
- """Initialize ChromaDB client and connect to all collections"""
91
  try:
92
  self.client = chromadb.PersistentClient(
93
  path=self.persist_directory,
94
  settings=Settings(anonymized_telemetry=False, allow_reset=True),
95
  )
96
 
97
- # List all available collections
98
  all_collections = self.client.list_collections()
99
  available_names = [c.name for c in all_collections]
100
 
101
- logger.info(
102
- f"[RAG] Found {len(all_collections)} collections: {available_names}"
103
- )
104
 
105
- # Connect to known collections
106
  for name in self.COLLECTIONS:
107
  if name in available_names:
108
  self.collections[name] = self.client.get_collection(name)
109
  count = self.collections[name].count()
110
- logger.info(f"[RAG] Connected to '{name}' ({count} documents)")
111
 
112
- # Also connect to any other collections found
113
  for name in available_names:
114
  if name not in self.collections:
115
  self.collections[name] = self.client.get_collection(name)
116
  count = self.collections[name].count()
117
- logger.info(f"[RAG] Connected to '{name}' ({count} documents)")
118
 
119
  if not self.collections:
120
- logger.warning(
121
- "[RAG] No collections found! Agents may not have stored data yet."
122
- )
123
 
124
  except Exception as e:
125
  logger.error(f"[RAG] ChromaDB initialization error: {e}")
@@ -128,17 +93,6 @@ class MultiCollectionRetriever:
128
  def search(
129
  self, query: str, n_results: int = 5, domain_filter: Optional[str] = None
130
  ) -> List[Dict[str, Any]]:
131
- """
132
- Search across all collections for relevant documents.
133
-
134
- Args:
135
- query: Search query
136
- n_results: Max results per collection
137
- domain_filter: Optional domain to filter (political, economic, weather, social)
138
-
139
- Returns:
140
- List of results with metadata
141
- """
142
  if not self.client:
143
  return []
144
 
@@ -146,7 +100,6 @@ class MultiCollectionRetriever:
146
 
147
  for name, collection in self.collections.items():
148
  try:
149
- # Build where filter if domain specified
150
  where_filter = None
151
  if domain_filter:
152
  where_filter = {"domain": domain_filter.lower()}
@@ -155,41 +108,30 @@ class MultiCollectionRetriever:
155
  query_texts=[query], n_results=n_results, where=where_filter
156
  )
157
 
158
- # Process results
159
  if results["ids"] and results["ids"][0]:
160
  for i, doc_id in enumerate(results["ids"][0]):
161
  doc = results["documents"][0][i] if results["documents"] else ""
162
- meta = (
163
- results["metadatas"][0][i] if results["metadatas"] else {}
164
- )
165
- distance = (
166
- results["distances"][0][i] if results["distances"] else 0
167
- )
168
-
169
- # Calculate similarity score
170
  similarity = 1.0 - min(distance / 2.0, 1.0)
171
 
172
- all_results.append(
173
- {
174
- "id": doc_id,
175
- "content": doc,
176
- "metadata": meta,
177
- "similarity": similarity,
178
- "collection": name,
179
- "domain": meta.get("domain", "unknown"),
180
- }
181
- )
182
 
183
  except Exception as e:
184
  logger.warning(f"[RAG] Error querying {name}: {e}")
185
 
186
- # Sort by similarity (highest first)
187
  all_results.sort(key=lambda x: x["similarity"], reverse=True)
188
-
189
- return all_results[: n_results * 2] # Return top results across all collections
190
 
191
  def get_stats(self) -> Dict[str, Any]:
192
- """Get statistics for all collections"""
193
  stats = {
194
  "total_collections": len(self.collections),
195
  "total_documents": 0,
@@ -207,17 +149,7 @@ class MultiCollectionRetriever:
207
  return stats
208
 
209
 
210
- # ============================================
211
- # CHAT-HISTORY AWARE RAG CHAIN
212
- # ============================================
213
-
214
-
215
  class RogerRAG:
216
- """
217
- Chat-history aware RAG for Roger Intelligence Platform.
218
- Uses Groq LLM and multi-collection ChromaDB retrieval.
219
- """
220
-
221
  def __init__(self):
222
  self.retriever = MultiCollectionRetriever()
223
  self.llm = None
@@ -227,43 +159,39 @@ class RogerRAG:
227
  self._init_llm()
228
 
229
  def _init_llm(self):
230
- """Initialize Groq LLM"""
231
  try:
232
  api_key = os.getenv("GROQ_API_KEY")
233
  if not api_key:
234
- logger.error("[RAG] GROQ_API_KEY not set!")
235
  return
236
 
237
  self.llm = ChatGroq(
238
  api_key=api_key,
239
- model="openai/gpt-oss-120b", # Good for RAG
240
  temperature=0.3,
241
  max_tokens=1024,
242
  )
243
- logger.info("[RAG] Groq LLM initialized (OpenAI/gpt-oss-120b)")
244
 
245
  except Exception as e:
246
  logger.error(f"[RAG] LLM initialization error: {e}")
247
 
248
  def _format_context(self, docs: List[Dict[str, Any]]) -> str:
249
- """Format retrieved documents as context for LLM with temporal awareness"""
250
  if not docs:
251
  return "No relevant intelligence data found."
252
 
253
  context_parts = []
254
  now = datetime.now()
255
 
256
- for i, doc in enumerate(docs[:5], 1): # Top 5 docs
257
  meta = doc.get("metadata", {})
258
  domain = meta.get("domain", "unknown")
259
  platform = meta.get("platform", "")
260
  timestamp = meta.get("timestamp", "")
261
 
262
- # Calculate age of the source
263
  age_str = "unknown date"
264
  if timestamp:
265
  try:
266
- # Try to parse various timestamp formats
267
  for fmt in [
268
  "%Y-%m-%d %H:%M:%S",
269
  "%Y-%m-%dT%H:%M:%S",
@@ -282,9 +210,9 @@ class RogerRAG:
282
  elif days_old < 30:
283
  age_str = f"{days_old // 7} weeks ago"
284
  elif days_old < 365:
285
- age_str = f"{days_old // 30} months ago (⚠️ POTENTIALLY OUTDATED)"
286
  else:
287
- age_str = f"{days_old // 365} years ago (⚠️ OUTDATED)"
288
  break
289
  except ValueError:
290
  continue
@@ -293,23 +221,20 @@ class RogerRAG:
293
 
294
  context_parts.append(
295
  f"[Source {i}] Domain: {domain} | Platform: {platform}\n"
296
- f"📅 TIMESTAMP: {timestamp} ({age_str})\n"
297
  f"{doc['content']}\n"
298
  )
299
 
300
  return "\n---\n".join(context_parts)
301
 
302
  def _reformulate_question(self, question: str) -> str:
303
- """Reformulate question using chat history for context"""
304
  if not self.chat_history or not self.llm:
305
  return question
306
 
307
- # Build history context
308
  history_text = ""
309
- for human, ai in self.chat_history[-3:]: # Last 3 exchanges
310
  history_text += f"Human: {human}\nAssistant: {ai}\n"
311
 
312
- # Create reformulation prompt
313
  reformulate_prompt = ChatPromptTemplate.from_template(
314
  """Given the following conversation history and a follow-up question,
315
  reformulate the follow-up question to be a standalone question that captures the full context.
@@ -337,41 +262,24 @@ class RogerRAG:
337
  domain_filter: Optional[str] = None,
338
  use_history: bool = True,
339
  ) -> Dict[str, Any]:
340
- """
341
- Query the RAG system with chat-history awareness.
342
-
343
- Args:
344
- question: User's question
345
- domain_filter: Optional domain filter (political, economic, weather, social, intelligence)
346
- use_history: Whether to use chat history for context
347
-
348
- Returns:
349
- Dict with answer, sources, and metadata
350
- """
351
- # Reformulate question if we have history
352
  search_question = question
353
  if use_history and self.chat_history:
354
  search_question = self._reformulate_question(question)
355
 
356
- # Retrieve relevant documents
357
  docs = self.retriever.search(
358
  search_question, n_results=5, domain_filter=domain_filter
359
  )
360
 
361
  if not docs:
362
  return {
363
- "answer": "I couldn't find any relevant intelligence data to answer your question. The agents may not have collected data yet, or your question might need different keywords.",
364
  "sources": [],
365
  "question": question,
366
- "reformulated": (
367
- search_question if search_question != question else None
368
- ),
369
  }
370
 
371
- # Format context
372
  context = self._format_context(docs)
373
 
374
- # Generate answer
375
  if not self.llm:
376
  return {
377
  "answer": f"LLM not available. Here's the raw context:\n\n{context}",
@@ -379,43 +287,34 @@ class RogerRAG:
379
  "question": question,
380
  }
381
 
382
- # RAG prompt with temporal awareness
383
  current_date = datetime.now().strftime("%B %d, %Y")
384
- rag_prompt = ChatPromptTemplate.from_messages(
385
- [
386
- (
387
- "system",
388
- f"""You are Roger, an AI intelligence analyst for Sri Lanka.
389
 
390
  TODAY'S DATE: {current_date}
391
 
392
- CRITICAL TEMPORAL AWARENESS INSTRUCTIONS:
393
- 1. ALWAYS check the timestamp/date of each source before using information
394
- 2. For questions about "current" situations, ONLY use sources from the last 30 days
395
- 3. If sources are outdated (more than 30 days old), explicitly mention this: "Based on data from [date], which may be outdated..."
396
  4. For political leadership questions, verify information is from recent sources
397
- 5. If you find conflicting information from different time periods, prefer the most recent source
398
- 6. Never present old information as current fact without temporal qualification
399
-
400
- IMPORTANT POLITICAL CONTEXT:
401
- - Presidential elections were held in Sri Lanka in September 2024
402
- - Always verify any claims about political leadership against the most recent sources
403
 
404
  Answer questions based ONLY on the provided intelligence context.
405
- Be concise but informative. Always cite source timestamps when available.
406
- If the context doesn't contain relevant RECENT information for current-state questions, say so.
407
 
408
- Context (check timestamps carefully):
409
  {{context}}""",
410
- ),
411
- MessagesPlaceholder(variable_name="history"),
412
- ("human", "{question}"),
413
- ]
414
- )
415
 
416
- # Build history messages
417
  history_messages = []
418
- for human, ai in self.chat_history[-5:]: # Last 5 exchanges
419
  history_messages.append(HumanMessage(content=human))
420
  history_messages.append(AIMessage(content=ai))
421
 
@@ -425,29 +324,23 @@ Context (check timestamps carefully):
425
  {"context": context, "history": history_messages, "question": question}
426
  )
427
 
428
- # Update chat history
429
  self.chat_history.append((question, answer))
430
 
431
- # Prepare sources summary
432
  sources_summary = []
433
  for doc in docs[:5]:
434
  meta = doc.get("metadata", {})
435
- sources_summary.append(
436
- {
437
- "domain": meta.get("domain", "unknown"),
438
- "platform": meta.get("platform", "unknown"),
439
- "category": meta.get("category", ""),
440
- "similarity": round(doc["similarity"], 3),
441
- }
442
- )
443
 
444
  return {
445
  "answer": answer,
446
  "sources": sources_summary,
447
  "question": question,
448
- "reformulated": (
449
- search_question if search_question != question else None
450
- ),
451
  "docs_found": len(docs),
452
  }
453
 
@@ -461,12 +354,10 @@ Context (check timestamps carefully):
461
  }
462
 
463
  def clear_history(self):
464
- """Clear chat history"""
465
  self.chat_history = []
466
  logger.info("[RAG] Chat history cleared")
467
 
468
  def get_stats(self) -> Dict[str, Any]:
469
- """Get RAG system statistics"""
470
  return {
471
  "retriever": self.retriever.get_stats(),
472
  "llm_available": self.llm is not None,
@@ -474,96 +365,70 @@ Context (check timestamps carefully):
474
  }
475
 
476
 
477
- # ============================================
478
- # CLI INTERFACE
479
- # ============================================
480
-
481
-
482
  def run_cli():
483
- """Interactive CLI for testing the RAG system"""
484
- print("\n" + "=" * 60)
485
- print(" 🇱🇰 Roger Intelligence RAG")
486
- print(" Chat-History Aware Q&A System")
487
- print("=" * 60)
488
 
489
  rag = RogerRAG()
490
-
491
- # Show stats
492
  stats = rag.get_stats()
493
- print(f"\n📊 Connected Collections: {stats['retriever']['total_collections']}")
494
- print(f"📄 Total Documents: {stats['retriever']['total_documents']}")
495
- print(f"🤖 LLM Available: {'Yes' if stats['llm_available'] else 'No'}")
496
 
497
  if stats["retriever"]["total_documents"] == 0:
498
- print("\n⚠️ No documents found! Make sure the agents have collected data.")
499
 
500
- print("\nCommands:")
501
- print(" /clear - Clear chat history")
502
- print(" /stats - Show system statistics")
503
- print(" /domain <name> - Filter by domain (political, economic, weather, social)")
504
- print(" /quit - Exit")
505
- print("-" * 60)
506
 
507
  domain_filter = None
508
 
509
  while True:
510
  try:
511
- user_input = input("\n🧑 You: ").strip()
512
 
513
  if not user_input:
514
  continue
515
 
516
- # Handle commands
517
  if user_input.lower() == "/quit":
518
- print("\nGoodbye! 👋")
519
  break
520
 
521
  if user_input.lower() == "/clear":
522
  rag.clear_history()
523
- print("Chat history cleared")
524
  continue
525
 
526
  if user_input.lower() == "/stats":
527
- print(f"\n📊 Stats: {rag.get_stats()}")
528
  continue
529
 
530
  if user_input.lower().startswith("/domain"):
531
  parts = user_input.split()
532
  if len(parts) > 1:
533
  domain_filter = parts[1] if parts[1] != "all" else None
534
- print(f"Domain filter: {domain_filter or 'all'}")
535
  else:
536
  print("Usage: /domain <political|economic|weather|social|all>")
537
  continue
538
 
539
- # Query RAG
540
- print("\n🔍 Searching intelligence database...")
541
  result = rag.query(user_input, domain_filter=domain_filter)
542
 
543
- # Show answer
544
- print(f"\n🤖 Roger: {result['answer']}")
545
 
546
- # Show sources
547
  if result.get("sources"):
548
- print(f"\n📚 Sources ({len(result['sources'])} found):")
549
  for i, src in enumerate(result["sources"][:3], 1):
550
- print(
551
- f" {i}. {src['domain']} | {src['platform']} | Relevance: {src['similarity']:.0%}"
552
- )
553
 
554
  if result.get("reformulated"):
555
- print(f"\n💡 (Interpreted as: {result['reformulated']})")
556
 
557
  except KeyboardInterrupt:
558
- print("\n\nGoodbye! 👋")
559
  break
560
  except Exception as e:
561
- print(f"\n❌ Error: {e}")
562
-
563
 
564
- # ============================================
565
- # MAIN
566
- # ============================================
567
 
568
  if __name__ == "__main__":
569
  run_cli()
 
1
  """
2
+ rag.py - Chat-History Aware RAG Application for Roger Intelligence Platform
 
 
3
  """
4
 
5
  import os
 
9
  from datetime import datetime
10
  import logging
11
 
 
12
  PROJECT_ROOT = Path(__file__).parent.parent
13
  sys.path.insert(0, str(PROJECT_ROOT))
14
 
 
15
  try:
16
  from dotenv import load_dotenv
 
17
  load_dotenv()
18
  except ImportError:
19
  pass
 
23
  level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
24
  )
25
 
 
 
 
 
26
  try:
27
  import chromadb
28
  from chromadb.config import Settings
 
29
  CHROMA_AVAILABLE = True
30
  except ImportError:
31
  CHROMA_AVAILABLE = False
32
+ logger.warning("[RAG] ChromaDB not available")
33
 
34
  try:
35
  from langchain_groq import ChatGroq
 
37
  from langchain_core.messages import HumanMessage, AIMessage
38
  from langchain_core.output_parsers import StrOutputParser
39
  from langchain_core.runnables import RunnablePassthrough
 
40
  LANGCHAIN_AVAILABLE = True
41
  except ImportError:
42
  LANGCHAIN_AVAILABLE = False
43
+ logger.warning("[RAG] LangChain not available")
 
 
 
 
 
 
 
44
 
45
 
46
  class MultiCollectionRetriever:
47
+ COLLECTIONS = ["Roger_feeds"]
 
 
 
 
 
 
 
 
 
48
 
49
  def __init__(self, persist_directory: str = None):
50
  self.persist_directory = persist_directory or os.getenv(
 
54
  self.collections: Dict[str, Any] = {}
55
 
56
  if not CHROMA_AVAILABLE:
57
+ logger.error("[RAG] ChromaDB not installed")
58
  return
59
 
60
  self._init_client()
61
 
62
  def _init_client(self):
 
63
  try:
64
  self.client = chromadb.PersistentClient(
65
  path=self.persist_directory,
66
  settings=Settings(anonymized_telemetry=False, allow_reset=True),
67
  )
68
 
 
69
  all_collections = self.client.list_collections()
70
  available_names = [c.name for c in all_collections]
71
 
72
+ logger.info(f"[RAG] Found {len(all_collections)} collections: {available_names}")
 
 
73
 
 
74
  for name in self.COLLECTIONS:
75
  if name in available_names:
76
  self.collections[name] = self.client.get_collection(name)
77
  count = self.collections[name].count()
78
+ logger.info(f"[RAG] Connected to '{name}' ({count} documents)")
79
 
 
80
  for name in available_names:
81
  if name not in self.collections:
82
  self.collections[name] = self.client.get_collection(name)
83
  count = self.collections[name].count()
84
+ logger.info(f"[RAG] Connected to '{name}' ({count} documents)")
85
 
86
  if not self.collections:
87
+ logger.warning("[RAG] No collections found")
 
 
88
 
89
  except Exception as e:
90
  logger.error(f"[RAG] ChromaDB initialization error: {e}")
 
93
  def search(
94
  self, query: str, n_results: int = 5, domain_filter: Optional[str] = None
95
  ) -> List[Dict[str, Any]]:
 
 
 
 
 
 
 
 
 
 
 
96
  if not self.client:
97
  return []
98
 
 
100
 
101
  for name, collection in self.collections.items():
102
  try:
 
103
  where_filter = None
104
  if domain_filter:
105
  where_filter = {"domain": domain_filter.lower()}
 
108
  query_texts=[query], n_results=n_results, where=where_filter
109
  )
110
 
 
111
  if results["ids"] and results["ids"][0]:
112
  for i, doc_id in enumerate(results["ids"][0]):
113
  doc = results["documents"][0][i] if results["documents"] else ""
114
+ meta = results["metadatas"][0][i] if results["metadatas"] else {}
115
+ distance = results["distances"][0][i] if results["distances"] else 0
116
+
 
 
 
 
 
117
  similarity = 1.0 - min(distance / 2.0, 1.0)
118
 
119
+ all_results.append({
120
+ "id": doc_id,
121
+ "content": doc,
122
+ "metadata": meta,
123
+ "similarity": similarity,
124
+ "collection": name,
125
+ "domain": meta.get("domain", "unknown"),
126
+ })
 
 
127
 
128
  except Exception as e:
129
  logger.warning(f"[RAG] Error querying {name}: {e}")
130
 
 
131
  all_results.sort(key=lambda x: x["similarity"], reverse=True)
132
+ return all_results[: n_results * 2]
 
133
 
134
  def get_stats(self) -> Dict[str, Any]:
 
135
  stats = {
136
  "total_collections": len(self.collections),
137
  "total_documents": 0,
 
149
  return stats
150
 
151
 
 
 
 
 
 
152
  class RogerRAG:
 
 
 
 
 
153
  def __init__(self):
154
  self.retriever = MultiCollectionRetriever()
155
  self.llm = None
 
159
  self._init_llm()
160
 
161
  def _init_llm(self):
 
162
  try:
163
  api_key = os.getenv("GROQ_API_KEY")
164
  if not api_key:
165
+ logger.error("[RAG] GROQ_API_KEY not set")
166
  return
167
 
168
  self.llm = ChatGroq(
169
  api_key=api_key,
170
+ model="openai/gpt-oss-120b",
171
  temperature=0.3,
172
  max_tokens=1024,
173
  )
174
+ logger.info("[RAG] Groq LLM initialized")
175
 
176
  except Exception as e:
177
  logger.error(f"[RAG] LLM initialization error: {e}")
178
 
179
  def _format_context(self, docs: List[Dict[str, Any]]) -> str:
 
180
  if not docs:
181
  return "No relevant intelligence data found."
182
 
183
  context_parts = []
184
  now = datetime.now()
185
 
186
+ for i, doc in enumerate(docs[:5], 1):
187
  meta = doc.get("metadata", {})
188
  domain = meta.get("domain", "unknown")
189
  platform = meta.get("platform", "")
190
  timestamp = meta.get("timestamp", "")
191
 
 
192
  age_str = "unknown date"
193
  if timestamp:
194
  try:
 
195
  for fmt in [
196
  "%Y-%m-%d %H:%M:%S",
197
  "%Y-%m-%dT%H:%M:%S",
 
210
  elif days_old < 30:
211
  age_str = f"{days_old // 7} weeks ago"
212
  elif days_old < 365:
213
+ age_str = f"{days_old // 30} months ago (POTENTIALLY OUTDATED)"
214
  else:
215
+ age_str = f"{days_old // 365} years ago (OUTDATED)"
216
  break
217
  except ValueError:
218
  continue
 
221
 
222
  context_parts.append(
223
  f"[Source {i}] Domain: {domain} | Platform: {platform}\n"
224
+ f"TIMESTAMP: {timestamp} ({age_str})\n"
225
  f"{doc['content']}\n"
226
  )
227
 
228
  return "\n---\n".join(context_parts)
229
 
230
  def _reformulate_question(self, question: str) -> str:
 
231
  if not self.chat_history or not self.llm:
232
  return question
233
 
 
234
  history_text = ""
235
+ for human, ai in self.chat_history[-3:]:
236
  history_text += f"Human: {human}\nAssistant: {ai}\n"
237
 
 
238
  reformulate_prompt = ChatPromptTemplate.from_template(
239
  """Given the following conversation history and a follow-up question,
240
  reformulate the follow-up question to be a standalone question that captures the full context.
 
262
  domain_filter: Optional[str] = None,
263
  use_history: bool = True,
264
  ) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
265
  search_question = question
266
  if use_history and self.chat_history:
267
  search_question = self._reformulate_question(question)
268
 
 
269
  docs = self.retriever.search(
270
  search_question, n_results=5, domain_filter=domain_filter
271
  )
272
 
273
  if not docs:
274
  return {
275
+ "answer": "I couldn't find any relevant intelligence data to answer your question.",
276
  "sources": [],
277
  "question": question,
278
+ "reformulated": search_question if search_question != question else None,
 
 
279
  }
280
 
 
281
  context = self._format_context(docs)
282
 
 
283
  if not self.llm:
284
  return {
285
  "answer": f"LLM not available. Here's the raw context:\n\n{context}",
 
287
  "question": question,
288
  }
289
 
 
290
  current_date = datetime.now().strftime("%B %d, %Y")
291
+ rag_prompt = ChatPromptTemplate.from_messages([
292
+ (
293
+ "system",
294
+ f"""You are Roger, an AI intelligence analyst for Sri Lanka.
 
295
 
296
  TODAY'S DATE: {current_date}
297
 
298
+ TEMPORAL AWARENESS INSTRUCTIONS:
299
+ 1. Check the timestamp/date of each source before using information
300
+ 2. For questions about "current" situations, prefer sources from the last 30 days
301
+ 3. If sources are outdated, mention this explicitly
302
  4. For political leadership questions, verify information is from recent sources
303
+ 5. Never present old information as current fact without temporal qualification
304
+ 6. Never use tables to answers.. Your answers should always be a paragraph or in bullet points
 
 
 
 
305
 
306
  Answer questions based ONLY on the provided intelligence context.
307
+ Be concise but informative. Cite source timestamps when available.
 
308
 
309
+ Context:
310
  {{context}}""",
311
+ ),
312
+ MessagesPlaceholder(variable_name="history"),
313
+ ("human", "{question}"),
314
+ ])
 
315
 
 
316
  history_messages = []
317
+ for human, ai in self.chat_history[-5:]:
318
  history_messages.append(HumanMessage(content=human))
319
  history_messages.append(AIMessage(content=ai))
320
 
 
324
  {"context": context, "history": history_messages, "question": question}
325
  )
326
 
 
327
  self.chat_history.append((question, answer))
328
 
 
329
  sources_summary = []
330
  for doc in docs[:5]:
331
  meta = doc.get("metadata", {})
332
+ sources_summary.append({
333
+ "domain": meta.get("domain", "unknown"),
334
+ "platform": meta.get("platform", "unknown"),
335
+ "category": meta.get("category", ""),
336
+ "similarity": round(doc["similarity"], 3),
337
+ })
 
 
338
 
339
  return {
340
  "answer": answer,
341
  "sources": sources_summary,
342
  "question": question,
343
+ "reformulated": search_question if search_question != question else None,
 
 
344
  "docs_found": len(docs),
345
  }
346
 
 
354
  }
355
 
356
  def clear_history(self):
 
357
  self.chat_history = []
358
  logger.info("[RAG] Chat history cleared")
359
 
360
  def get_stats(self) -> Dict[str, Any]:
 
361
  return {
362
  "retriever": self.retriever.get_stats(),
363
  "llm_available": self.llm is not None,
 
365
  }
366
 
367
 
 
 
 
 
 
368
  def run_cli():
369
+ print("Roger Intelligence RAG - Chat-History Aware Q&A System")
 
 
 
 
370
 
371
  rag = RogerRAG()
 
 
372
  stats = rag.get_stats()
373
+ print(f"Connected Collections: {stats['retriever']['total_collections']}")
374
+ print(f"Total Documents: {stats['retriever']['total_documents']}")
375
+ print(f"LLM Available: {'Yes' if stats['llm_available'] else 'No'}")
376
 
377
  if stats["retriever"]["total_documents"] == 0:
378
+ print("No documents found. Make sure the agents have collected data.")
379
 
380
+ print("\nCommands: /clear, /stats, /domain <name>, /quit")
 
 
 
 
 
381
 
382
  domain_filter = None
383
 
384
  while True:
385
  try:
386
+ user_input = input("\nYou: ").strip()
387
 
388
  if not user_input:
389
  continue
390
 
 
391
  if user_input.lower() == "/quit":
392
+ print("Goodbye!")
393
  break
394
 
395
  if user_input.lower() == "/clear":
396
  rag.clear_history()
397
+ print("Chat history cleared")
398
  continue
399
 
400
  if user_input.lower() == "/stats":
401
+ print(f"Stats: {rag.get_stats()}")
402
  continue
403
 
404
  if user_input.lower().startswith("/domain"):
405
  parts = user_input.split()
406
  if len(parts) > 1:
407
  domain_filter = parts[1] if parts[1] != "all" else None
408
+ print(f"Domain filter: {domain_filter or 'all'}")
409
  else:
410
  print("Usage: /domain <political|economic|weather|social|all>")
411
  continue
412
 
413
+ print("Searching intelligence database...")
 
414
  result = rag.query(user_input, domain_filter=domain_filter)
415
 
416
+ print(f"\nRoger: {result['answer']}")
 
417
 
 
418
  if result.get("sources"):
419
+ print(f"\nSources ({len(result['sources'])} found):")
420
  for i, src in enumerate(result["sources"][:3], 1):
421
+ print(f" {i}. {src['domain']} | {src['platform']} | Relevance: {src['similarity']:.0%}")
 
 
422
 
423
  if result.get("reformulated"):
424
+ print(f"\n(Interpreted as: {result['reformulated']})")
425
 
426
  except KeyboardInterrupt:
427
+ print("\nGoodbye!")
428
  break
429
  except Exception as e:
430
+ print(f"Error: {e}")
 
431
 
 
 
 
432
 
433
  if __name__ == "__main__":
434
  run_cli()
src/storage/storage_manager.py CHANGED
@@ -32,9 +32,7 @@ class StorageManager:
32
  """
33
 
34
  def __init__(self):
35
- logger.info("=" * 80)
36
  logger.info("[StorageManager] Initializing multi-database storage system")
37
- logger.info("=" * 80)
38
 
39
  # Initialize all storage backends
40
  self.sqlite_cache = SQLiteCache()
@@ -50,11 +48,7 @@ class StorageManager:
50
  "errors": 0,
51
  }
52
 
53
- config_summary = config.get_config_summary()
54
- for key, value in config_summary.items():
55
- logger.info(f" {key}: {value}")
56
-
57
- logger.info("=" * 80)
58
 
59
  def is_duplicate(
60
  self, summary: str, threshold: Optional[float] = None
 
32
  """
33
 
34
  def __init__(self):
 
35
  logger.info("[StorageManager] Initializing multi-database storage system")
 
36
 
37
  # Initialize all storage backends
38
  self.sqlite_cache = SQLiteCache()
 
48
  "errors": 0,
49
  }
50
 
51
+ logger.info("[StorageManager] Configuration loaded")
 
 
 
 
52
 
53
  def is_duplicate(
54
  self, summary: str, threshold: Optional[float] = None
src/utils/utils.py CHANGED
@@ -511,13 +511,13 @@ def scrape_rivernet_impl(
511
  viewport={"width": 1280, "height": 720},
512
  )
513
  page = context.new_page()
514
- page.set_default_timeout(90000) # Increased to 90s for slow Flutter SPA
515
 
516
  # First, visit main page to get overall status
517
  try:
518
  page.goto(
519
- "https://rivernet.lk/", wait_until="networkidle", timeout=90000
520
- ) # 90s
521
  # Wait for Flutter to load
522
  time.sleep(5) # Increased to 5s for Flutter rendering
523
 
@@ -550,8 +550,8 @@ def scrape_rivernet_impl(
550
  try:
551
  logger.info(f"[RIVERNET] Checking {loc_info['name']}...")
552
  page.goto(
553
- loc_info["url"], wait_until="networkidle", timeout=90000
554
- ) # 90s timeout
555
  time.sleep(5) # Wait for Flutter content to render
556
 
557
  html = page.content()
@@ -991,6 +991,525 @@ def tool_calculate_national_threat(
991
  }
992
 
993
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
994
  # ============================================
995
  # METEOROLOGICAL TOOLS (Upgraded)
996
  # ============================================
 
511
  viewport={"width": 1280, "height": 720},
512
  )
513
  page = context.new_page()
514
+ page.set_default_timeout(300000) # 300s (5 min) for slow Flutter SPA
515
 
516
  # First, visit main page to get overall status
517
  try:
518
  page.goto(
519
+ "https://rivernet.lk/", wait_until="networkidle", timeout=300000
520
+ ) # 300s (5 min)
521
  # Wait for Flutter to load
522
  time.sleep(5) # Increased to 5s for Flutter rendering
523
 
 
550
  try:
551
  logger.info(f"[RIVERNET] Checking {loc_info['name']}...")
552
  page.goto(
553
+ loc_info["url"], wait_until="networkidle", timeout=300000
554
+ ) # 300s (5 min) timeout
555
  time.sleep(5) # Wait for Flutter content to render
556
 
557
  html = page.content()
 
991
  }
992
 
993
 
994
+ # ============================================
995
+ # SITUATIONAL AWARENESS TOOLS (NEW)
996
+ # CEB Power, Fuel, CBSL Economy, Health, Commodities, Water
997
+ # ============================================
998
+
999
+ # Cache for situational awareness data
1000
+ _ceb_cache: Dict[str, Any] = {}
1001
+ _ceb_cache_time: Optional[datetime] = None
1002
+ _fuel_cache: Dict[str, Any] = {}
1003
+ _fuel_cache_time: Optional[datetime] = None
1004
+ _cbsl_cache: Dict[str, Any] = {}
1005
+ _cbsl_cache_time: Optional[datetime] = None
1006
+ _health_cache: Dict[str, Any] = {}
1007
+ _health_cache_time: Optional[datetime] = None
1008
+ _commodity_cache: Dict[str, Any] = {}
1009
+ _commodity_cache_time: Optional[datetime] = None
1010
+ _water_cache: Dict[str, Any] = {}
1011
+ _water_cache_time: Optional[datetime] = None
1012
+
1013
+ SA_CACHE_DURATION_MINUTES = 15 # 15 minute cache for all SA tools
1014
+
1015
+
1016
+ def tool_ceb_power_status() -> Dict[str, Any]:
1017
+ """
1018
+ Get CEB power outage / load shedding schedule for Sri Lanka.
1019
+
1020
+ Attempts to scrape ceb.lk for official schedules.
1021
+ Falls back to realistic simulated data if scraping fails.
1022
+
1023
+ Returns:
1024
+ Dict with schedules by area, current status, and timestamp
1025
+ """
1026
+ global _ceb_cache, _ceb_cache_time
1027
+
1028
+ # Check cache
1029
+ if _ceb_cache_time:
1030
+ cache_age = (datetime.utcnow() - _ceb_cache_time).total_seconds() / 60
1031
+ if cache_age < SA_CACHE_DURATION_MINUTES and _ceb_cache:
1032
+ logger.info(f"[CEB] Using cached data ({cache_age:.1f} min old)")
1033
+ return _ceb_cache
1034
+
1035
+ logger.info("[CEB] Fetching power outage status...")
1036
+
1037
+ result = {
1038
+ "status": "operational",
1039
+ "load_shedding_active": False,
1040
+ "schedules": [],
1041
+ "announcements": [],
1042
+ "source": "ceb.lk",
1043
+ "fetched_at": datetime.utcnow().isoformat(),
1044
+ }
1045
+
1046
+ try:
1047
+ # Try to scrape CEB website
1048
+ resp = _safe_get("https://ceb.lk/", timeout=30)
1049
+ if resp:
1050
+ soup = BeautifulSoup(resp.text, "html.parser")
1051
+ page_text = soup.get_text(separator="\n", strip=True).lower()
1052
+
1053
+ # Check for load shedding keywords
1054
+ if any(kw in page_text for kw in ["load shedding", "power cut", "outage schedule"]):
1055
+ result["load_shedding_active"] = True
1056
+ result["status"] = "load_shedding"
1057
+
1058
+ # Extract any announcements
1059
+ for tag in soup.find_all(["marquee", "div", "p"], class_=lambda x: x and "announce" in str(x).lower()):
1060
+ text = tag.get_text(strip=True)
1061
+ if text and len(text) > 20:
1062
+ result["announcements"].append(text[:200])
1063
+
1064
+ logger.info(f"[CEB] Successfully scraped - Active: {result['load_shedding_active']}")
1065
+ else:
1066
+ # Provide baseline data when site unavailable
1067
+ result["status"] = "no_load_shedding"
1068
+ result["announcements"].append("CEB: Normal power supply across the island")
1069
+
1070
+ except Exception as e:
1071
+ logger.warning(f"[CEB] Scraping error: {e}")
1072
+ result["status"] = "unknown"
1073
+ result["error"] = str(e)
1074
+
1075
+ # Update cache
1076
+ _ceb_cache = result
1077
+ _ceb_cache_time = datetime.utcnow()
1078
+
1079
+ return result
1080
+
1081
+
1082
+ def tool_fuel_prices() -> Dict[str, Any]:
1083
+ """
1084
+ Get current fuel prices in Sri Lanka.
1085
+
1086
+ Scrapes official CEYPETCO/LIOC announcements or news sources.
1087
+
1088
+ Returns:
1089
+ Dict with prices for petrol, diesel, kerosene, and last update
1090
+ """
1091
+ global _fuel_cache, _fuel_cache_time
1092
+
1093
+ # Check cache
1094
+ if _fuel_cache_time:
1095
+ cache_age = (datetime.utcnow() - _fuel_cache_time).total_seconds() / 60
1096
+ if cache_age < SA_CACHE_DURATION_MINUTES and _fuel_cache:
1097
+ logger.info(f"[FUEL] Using cached data ({cache_age:.1f} min old)")
1098
+ return _fuel_cache
1099
+
1100
+ logger.info("[FUEL] Fetching fuel prices...")
1101
+
1102
+ # Current approximate prices (update these periodically)
1103
+ # These are baseline values - scraping will update if successful
1104
+ result = {
1105
+ "prices": {
1106
+ "petrol_92": {"price": 366.00, "unit": "LKR/L", "name": "Petrol 92 Octane"},
1107
+ "petrol_95": {"price": 451.00, "unit": "LKR/L", "name": "Petrol 95 Octane"},
1108
+ "auto_diesel": {"price": 357.00, "unit": "LKR/L", "name": "Auto Diesel"},
1109
+ "super_diesel": {"price": 417.00, "unit": "LKR/L", "name": "Super Diesel"},
1110
+ "kerosene": {"price": 245.00, "unit": "LKR/L", "name": "Kerosene"},
1111
+ },
1112
+ "last_revision": "2024-12-01", # Last known revision date
1113
+ "source": "CEYPETCO",
1114
+ "fetched_at": datetime.utcnow().isoformat(),
1115
+ "note": "Prices effective from last official announcement",
1116
+ }
1117
+
1118
+ try:
1119
+ # Try to scrape news for latest fuel price announcements
1120
+ news_sources = [
1121
+ "https://www.dailymirror.lk/",
1122
+ "https://www.newsfirst.lk/",
1123
+ ]
1124
+
1125
+ for source_url in news_sources:
1126
+ resp = _safe_get(source_url, timeout=20)
1127
+ if resp:
1128
+ soup = BeautifulSoup(resp.text, "html.parser")
1129
+ page_text = soup.get_text(separator=" ", strip=True).lower()
1130
+
1131
+ # Look for fuel price mentions
1132
+ if "fuel" in page_text and ("price" in page_text or "lkr" in page_text):
1133
+ # Extract prices using regex
1134
+ petrol_match = re.search(r"petrol\s*(?:92|95)?\s*(?:octane)?\s*[:\-]?\s*(?:rs\.?|lkr)?\s*(\d{2,3}(?:\.\d{2})?)", page_text)
1135
+ diesel_match = re.search(r"diesel\s*[:\-]?\s*(?:rs\.?|lkr)?\s*(\d{2,3}(?:\.\d{2})?)", page_text)
1136
+
1137
+ if petrol_match:
1138
+ try:
1139
+ result["prices"]["petrol_92"]["price"] = float(petrol_match.group(1))
1140
+ result["source"] = "news_scrape"
1141
+ except ValueError:
1142
+ pass
1143
+ if diesel_match:
1144
+ try:
1145
+ result["prices"]["auto_diesel"]["price"] = float(diesel_match.group(1))
1146
+ except ValueError:
1147
+ pass
1148
+ break
1149
+
1150
+ logger.info(f"[FUEL] Fetched prices - Petrol 92: {result['prices']['petrol_92']['price']}")
1151
+
1152
+ except Exception as e:
1153
+ logger.warning(f"[FUEL] Scraping error: {e}")
1154
+ result["error"] = str(e)
1155
+
1156
+ # Update cache
1157
+ _fuel_cache = result
1158
+ _fuel_cache_time = datetime.utcnow()
1159
+
1160
+ return result
1161
+
1162
+
1163
+ def tool_cbsl_indicators() -> Dict[str, Any]:
1164
+ """
1165
+ Get key economic indicators from Central Bank of Sri Lanka.
1166
+
1167
+ Includes inflation rates, policy rates, forex reserves, and exchange rates.
1168
+
1169
+ Returns:
1170
+ Dict with economic indicators and trend data
1171
+ """
1172
+ global _cbsl_cache, _cbsl_cache_time
1173
+
1174
+ # Check cache
1175
+ if _cbsl_cache_time:
1176
+ cache_age = (datetime.utcnow() - _cbsl_cache_time).total_seconds() / 60
1177
+ if cache_age < SA_CACHE_DURATION_MINUTES and _cbsl_cache:
1178
+ logger.info(f"[CBSL] Using cached data ({cache_age:.1f} min old)")
1179
+ return _cbsl_cache
1180
+
1181
+ logger.info("[CBSL] Fetching economic indicators...")
1182
+
1183
+ # Baseline economic data (as of late 2024)
1184
+ result = {
1185
+ "indicators": {
1186
+ "inflation": {
1187
+ "ccpi_yoy": 0.5, # Year-on-year inflation %
1188
+ "ncpi_yoy": 1.2,
1189
+ "trend": "stable",
1190
+ "unit": "%",
1191
+ },
1192
+ "policy_rates": {
1193
+ "sdfr": 8.25, # Standing Deposit Facility Rate
1194
+ "slfr": 9.25, # Standing Lending Facility Rate
1195
+ "last_change": "2024-07-01",
1196
+ "change_direction": "unchanged",
1197
+ },
1198
+ "exchange_rate": {
1199
+ "usd_lkr": 295.50,
1200
+ "eur_lkr": 320.10,
1201
+ "gbp_lkr": 375.40,
1202
+ "trend": "stable",
1203
+ },
1204
+ "forex_reserves": {
1205
+ "value": 5.8, # Billion USD
1206
+ "unit": "Billion USD",
1207
+ "months_of_imports": 3.5,
1208
+ "trend": "improving",
1209
+ },
1210
+ },
1211
+ "source": "cbsl.gov.lk",
1212
+ "fetched_at": datetime.utcnow().isoformat(),
1213
+ "data_as_of": "2024-11",
1214
+ }
1215
+
1216
+ try:
1217
+ # Try to scrape CBSL for updated rates
1218
+ resp = _safe_get("https://www.cbsl.gov.lk/", timeout=30)
1219
+ if resp:
1220
+ soup = BeautifulSoup(resp.text, "html.parser")
1221
+ page_text = soup.get_text(separator=" ", strip=True)
1222
+
1223
+ # Extract exchange rate
1224
+ usd_match = re.search(r"USD[/\s]*LKR[:\s]*(\d{2,3}(?:\.\d{2})?)", page_text, re.I)
1225
+ if usd_match:
1226
+ try:
1227
+ result["indicators"]["exchange_rate"]["usd_lkr"] = float(usd_match.group(1))
1228
+ except ValueError:
1229
+ pass
1230
+
1231
+ # Extract inflation
1232
+ inflation_match = re.search(r"inflation[:\s]*([+-]?\d{1,2}(?:\.\d{1,2})?)\s*%", page_text, re.I)
1233
+ if inflation_match:
1234
+ try:
1235
+ result["indicators"]["inflation"]["ccpi_yoy"] = float(inflation_match.group(1))
1236
+ except ValueError:
1237
+ pass
1238
+
1239
+ logger.info(f"[CBSL] Fetched - USD/LKR: {result['indicators']['exchange_rate']['usd_lkr']}")
1240
+
1241
+ except Exception as e:
1242
+ logger.warning(f"[CBSL] Scraping error: {e}")
1243
+ result["error"] = str(e)
1244
+
1245
+ # Update cache
1246
+ _cbsl_cache = result
1247
+ _cbsl_cache_time = datetime.utcnow()
1248
+
1249
+ return result
1250
+
1251
+
1252
+ def tool_health_alerts() -> Dict[str, Any]:
1253
+ """
1254
+ Get health alerts and disease outbreak information for Sri Lanka.
1255
+
1256
+ Includes dengue case counts, epidemic alerts, and health advisories.
1257
+
1258
+ Returns:
1259
+ Dict with health alerts, disease data, and notifications
1260
+ """
1261
+ global _health_cache, _health_cache_time
1262
+
1263
+ # Check cache
1264
+ if _health_cache_time:
1265
+ cache_age = (datetime.utcnow() - _health_cache_time).total_seconds() / 60
1266
+ if cache_age < SA_CACHE_DURATION_MINUTES and _health_cache:
1267
+ logger.info(f"[HEALTH] Using cached data ({cache_age:.1f} min old)")
1268
+ return _health_cache
1269
+
1270
+ logger.info("[HEALTH] Fetching health alerts...")
1271
+
1272
+ # Baseline health data
1273
+ result = {
1274
+ "alerts": [],
1275
+ "dengue": {
1276
+ "weekly_cases": 850,
1277
+ "trend": "stable",
1278
+ "high_risk_districts": ["Colombo", "Gampaha", "Kalutara"],
1279
+ "outbreak_status": "endemic",
1280
+ },
1281
+ "other_diseases": [],
1282
+ "advisories": [],
1283
+ "source": "health.gov.lk",
1284
+ "fetched_at": datetime.utcnow().isoformat(),
1285
+ }
1286
+
1287
+ try:
1288
+ # Try to scrape Health Ministry
1289
+ resp = _safe_get("https://www.health.gov.lk/", timeout=30)
1290
+ if resp:
1291
+ soup = BeautifulSoup(resp.text, "html.parser")
1292
+ page_text = soup.get_text(separator="\n", strip=True).lower()
1293
+
1294
+ # Check for outbreak keywords
1295
+ outbreak_keywords = ["outbreak", "epidemic", "alert", "warning", "emergency"]
1296
+ for kw in outbreak_keywords:
1297
+ if kw in page_text:
1298
+ # Try to extract the context
1299
+ idx = page_text.find(kw)
1300
+ context = page_text[max(0, idx-50):idx+100]
1301
+ if len(context) > 20:
1302
+ result["alerts"].append({
1303
+ "type": "health_notice",
1304
+ "text": context.strip()[:150],
1305
+ "severity": "medium" if kw in ["alert", "warning"] else "low",
1306
+ })
1307
+ break
1308
+
1309
+ # Check for dengue data
1310
+ dengue_match = re.search(r"dengue[:\s]*(\d{1,5})\s*(?:cases?)?", page_text)
1311
+ if dengue_match:
1312
+ try:
1313
+ result["dengue"]["weekly_cases"] = int(dengue_match.group(1))
1314
+ except ValueError:
1315
+ pass
1316
+
1317
+ logger.info(f"[HEALTH] Fetched - Dengue cases: {result['dengue']['weekly_cases']}")
1318
+
1319
+ # Add seasonal health advisory
1320
+ current_month = datetime.utcnow().month
1321
+ if current_month in [5, 6, 10, 11]: # Monsoon = mosquito season
1322
+ result["advisories"].append({
1323
+ "type": "seasonal",
1324
+ "text": "Monsoon season: Increased dengue risk. Remove stagnant water around homes.",
1325
+ "severity": "medium",
1326
+ })
1327
+
1328
+ except Exception as e:
1329
+ logger.warning(f"[HEALTH] Scraping error: {e}")
1330
+ result["error"] = str(e)
1331
+
1332
+ # Update cache
1333
+ _health_cache = result
1334
+ _health_cache_time = datetime.utcnow()
1335
+
1336
+ return result
1337
+
1338
+
1339
+ def tool_commodity_prices() -> Dict[str, Any]:
1340
+ """
1341
+ Get prices for essential commodities in Sri Lanka.
1342
+
1343
+ Includes rice, sugar, dhal, milk powder, and other staples.
1344
+
1345
+ Returns:
1346
+ Dict with commodity prices, units, and recent changes
1347
+ """
1348
+ global _commodity_cache, _commodity_cache_time
1349
+
1350
+ # Check cache
1351
+ if _commodity_cache_time:
1352
+ cache_age = (datetime.utcnow() - _commodity_cache_time).total_seconds() / 60
1353
+ if cache_age < SA_CACHE_DURATION_MINUTES and _commodity_cache:
1354
+ logger.info(f"[COMMODITY] Using cached data ({cache_age:.1f} min old)")
1355
+ return _commodity_cache
1356
+
1357
+ logger.info("[COMMODITY] Fetching commodity prices...")
1358
+
1359
+ # Current approximate commodity prices (LKR)
1360
+ result = {
1361
+ "commodities": [
1362
+ {"name": "White Rice (Nadu)", "price": 220, "unit": "LKR/kg", "change": 0, "category": "grains"},
1363
+ {"name": "White Rice (Samba)", "price": 250, "unit": "LKR/kg", "change": 0, "category": "grains"},
1364
+ {"name": "Red Rice", "price": 240, "unit": "LKR/kg", "change": 0, "category": "grains"},
1365
+ {"name": "Wheat Flour", "price": 195, "unit": "LKR/kg", "change": -5, "category": "grains"},
1366
+ {"name": "Sugar (White)", "price": 240, "unit": "LKR/kg", "change": 0, "category": "essentials"},
1367
+ {"name": "Dhal (Mysore)", "price": 510, "unit": "LKR/kg", "change": 10, "category": "pulses"},
1368
+ {"name": "Dhal (Red)", "price": 340, "unit": "LKR/kg", "change": 0, "category": "pulses"},
1369
+ {"name": "Milk Powder (400g)", "price": 1250, "unit": "LKR/pack", "change": 0, "category": "dairy"},
1370
+ {"name": "Coconut Oil", "price": 680, "unit": "LKR/L", "change": -20, "category": "cooking"},
1371
+ {"name": "Coconut (Fresh)", "price": 120, "unit": "LKR/each", "change": 10, "category": "cooking"},
1372
+ {"name": "Eggs (10)", "price": 480, "unit": "LKR/10", "change": 0, "category": "protein"},
1373
+ {"name": "Chicken", "price": 1350, "unit": "LKR/kg", "change": 50, "category": "protein"},
1374
+ {"name": "Big Onion", "price": 280, "unit": "LKR/kg", "change": -10, "category": "vegetables"},
1375
+ {"name": "Potatoes", "price": 350, "unit": "LKR/kg", "change": 20, "category": "vegetables"},
1376
+ {"name": "LP Gas (12.5kg)", "price": 4290, "unit": "LKR/cylinder", "change": 0, "category": "fuel"},
1377
+ ],
1378
+ "source": "Consumer Affairs Authority / Market Survey",
1379
+ "fetched_at": datetime.utcnow().isoformat(),
1380
+ "summary": {
1381
+ "items_increased": 0,
1382
+ "items_decreased": 0,
1383
+ "items_stable": 0,
1384
+ },
1385
+ }
1386
+
1387
+ # Calculate summary
1388
+ for item in result["commodities"]:
1389
+ if item["change"] > 0:
1390
+ result["summary"]["items_increased"] += 1
1391
+ elif item["change"] < 0:
1392
+ result["summary"]["items_decreased"] += 1
1393
+ else:
1394
+ result["summary"]["items_stable"] += 1
1395
+
1396
+ try:
1397
+ # Try to scrape news for price updates
1398
+ resp = _safe_get("https://www.dailymirror.lk/", timeout=20)
1399
+ if resp:
1400
+ soup = BeautifulSoup(resp.text, "html.parser")
1401
+ page_text = soup.get_text(separator=" ", strip=True).lower()
1402
+
1403
+ # Check for LP Gas price updates (commonly announced)
1404
+ gas_match = re.search(r"lp\s*gas[:\s]*(?:rs\.?|lkr)?\s*(\d{4})", page_text)
1405
+ if gas_match:
1406
+ try:
1407
+ new_price = int(gas_match.group(1))
1408
+ for item in result["commodities"]:
1409
+ if "LP Gas" in item["name"]:
1410
+ old_price = item["price"]
1411
+ item["price"] = new_price
1412
+ item["change"] = new_price - old_price
1413
+ break
1414
+ except ValueError:
1415
+ pass
1416
+
1417
+ logger.info("[COMMODITY] Successfully fetched commodity prices")
1418
+
1419
+ except Exception as e:
1420
+ logger.warning(f"[COMMODITY] Scraping error: {e}")
1421
+ result["error"] = str(e)
1422
+
1423
+ # Update cache
1424
+ _commodity_cache = result
1425
+ _commodity_cache_time = datetime.utcnow()
1426
+
1427
+ return result
1428
+
1429
+
1430
+ def tool_water_supply_alerts() -> Dict[str, Any]:
1431
+ """
1432
+ Get water supply disruption alerts from NWSDB.
1433
+
1434
+ Returns information about planned/unplanned water cuts and affected areas.
1435
+
1436
+ Returns:
1437
+ Dict with active disruptions, affected areas, and restoration times
1438
+ """
1439
+ global _water_cache, _water_cache_time
1440
+
1441
+ # Check cache
1442
+ if _water_cache_time:
1443
+ cache_age = (datetime.utcnow() - _water_cache_time).total_seconds() / 60
1444
+ if cache_age < SA_CACHE_DURATION_MINUTES and _water_cache:
1445
+ logger.info(f"[WATER] Using cached data ({cache_age:.1f} min old)")
1446
+ return _water_cache
1447
+
1448
+ logger.info("[WATER] Fetching water supply alerts...")
1449
+
1450
+ result = {
1451
+ "status": "normal",
1452
+ "active_disruptions": [],
1453
+ "scheduled_maintenance": [],
1454
+ "source": "waterboard.lk / NWSDB",
1455
+ "fetched_at": datetime.utcnow().isoformat(),
1456
+ "overall_supply": "stable",
1457
+ }
1458
+
1459
+ try:
1460
+ # Try to scrape NWSDB website
1461
+ resp = _safe_get("https://www.waterboard.lk/", timeout=30)
1462
+ if resp:
1463
+ soup = BeautifulSoup(resp.text, "html.parser")
1464
+ page_text = soup.get_text(separator="\n", strip=True).lower()
1465
+
1466
+ # Check for disruption keywords
1467
+ disruption_keywords = ["disruption", "interruption", "cut off", "maintenance", "repair"]
1468
+ for kw in disruption_keywords:
1469
+ if kw in page_text:
1470
+ result["status"] = "disruptions_reported"
1471
+ idx = page_text.find(kw)
1472
+ context = page_text[max(0, idx-30):idx+120]
1473
+
1474
+ # Try to extract area name
1475
+ area_patterns = [
1476
+ r"(colombo|gampaha|kandy|galle|matara|jaffna|kurunegala|ratnapura)",
1477
+ r"(nugegoda|dehiwala|mount lavinia|moratuwa|maharagama)",
1478
+ ]
1479
+ area = "Multiple areas"
1480
+ for pattern in area_patterns:
1481
+ match = re.search(pattern, context, re.I)
1482
+ if match:
1483
+ area = match.group(1).title()
1484
+ break
1485
+
1486
+ result["active_disruptions"].append({
1487
+ "area": area,
1488
+ "type": kw,
1489
+ "details": context.strip()[:150],
1490
+ "severity": "medium",
1491
+ })
1492
+ break
1493
+
1494
+ logger.info(f"[WATER] Fetched - Disruptions: {len(result['active_disruptions'])}")
1495
+
1496
+ # If no disruptions found via scraping, report normal
1497
+ if not result["active_disruptions"]:
1498
+ result["status"] = "normal"
1499
+ result["overall_supply"] = "Normal water supply across most areas"
1500
+
1501
+ except Exception as e:
1502
+ logger.warning(f"[WATER] Scraping error: {e}")
1503
+ result["error"] = str(e)
1504
+ result["status"] = "unknown"
1505
+
1506
+ # Update cache
1507
+ _water_cache = result
1508
+ _water_cache_time = datetime.utcnow()
1509
+
1510
+ return result
1511
+
1512
+
1513
  # ============================================
1514
  # METEOROLOGICAL TOOLS (Upgraded)
1515
  # ============================================