File size: 4,762 Bytes
021e065
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Dict, Any

class CommunitySaccoClassifier(nn.Module):
    def __init__(self, input_dim=10, hidden_dim=24):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 3)  # 3 classes: Stable, Caution, At Risk
        )
    def forward(self, x):
        return self.net(x)

class SentiCommunityEngine:
    def __init__(self, weights_path: str):
        self.model = CommunitySaccoClassifier()
        if os.path.exists(weights_path):
            self.model.load_state_dict(torch.load(weights_path, map_location=torch.device('cpu')))
        self.model.eval()

    def predict(self, text: str) -> Dict[str, Any]:
        raw = text.lower()
        
        feat = [
            len(raw) / 10000.0,
            1.0 if "sacco" in raw else 0.0,
            1.0 if "dividend" in raw else 0.0,
            1.0 if "microfinance" in raw else 0.0,
            1.0 if "literacy" in raw else 0.0,
            1.0 if "expenditure" in raw else 0.0,
            1.0 if "budget" in raw else 0.0,
            1.0 if "swahili" in raw else 0.0,
            1.0 if "sheng" in raw else 0.0,
            1.0 if "behavioral" in raw else 0.0
        ]
        
        with torch.no_grad():
            x = torch.tensor([feat], dtype=torch.float32)
            logits = self.model(x)
            probs = F.softmax(logits, dim=1).numpy()[0]
            pred_class = int(logits.argmax(dim=1).item())
            
        health_states = ["Stable / Liquid", "Caution / Review dividend payouts", "At Risk / High NPLs"]
        nudge = "Weka akiba leo kwa maisha bora ya baadaye."
        
        if pred_class == 2:
            nudge = "Chunga mfuko wako! Punguza madeni na ongeza hisa zako katika SACCO."
        elif pred_class == 1:
            nudge = "Tenga bajeti vizuri ili uweze kulipa mkopo wako bila shida."
            
        return {
            "sacco_health_status": health_states[pred_class],
            "nudge_message": nudge,
            "confidence": float(probs[pred_class]),
            "framework": "SASRA Sacco Regulations & Financial Literacy Guidelines"
        }


# ── RLM Integration ──────────────────────────────────────────────

class SentiCommunityRLM:
    """
    SentiCommunityRLM wraps the shared RLMEngine to provide deep reasoning capabilities
    using a dedicated Ollama specialist model (senti-community-rlm).
    """
    def __init__(self, model_name: str = "senti-community-rlm"):
        import sys
        import os
        base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        if base_dir not in sys.path:
            sys.path.insert(0, base_dir)
            
        from senti.core.engines.superpacks.rlm_engine import RLMEngine
        self.engine = RLMEngine(model=model_name)

    async def predict_deep(self, text: str, tier: str = "C") -> dict:
        import time
        context = {
            "tier": tier,
            "domain": "senticommunity",
            "timestamp": time.time(),
        }
        
        system_suffix = (
            "Focus on SACCO dividend planning, public budget expenditure explainers, financial literacy teaching, and Sheng/Swahili cognitive behavioral spend nudges."
        )
        
        rlm_response = await self.engine.reason(
            query=text,
            context=context,
            system_suffix=system_suffix
        )
        
        res = rlm_response.to_dict()
        
        # Map RLM decision to legacy SML fields for backward compatibility
        decision = res.get("decision", "stable").lower()
        health_status = "Stable / Liquid"
        nudge = "Weka akiba leo kwa maisha bora ya baadaye."
        
        if any(w in decision for w in ["risk", "poor", "insolvent", "alert"]):
            health_status = "At Risk / High NPLs"
            nudge = "Chunga mfuko wako! Punguza madeni na ongeza hisa zako katika SACCO."
        elif any(w in decision for w in ["caution", "review", "moderate"]):
            health_status = "Caution / Review dividend payouts"
            nudge = "Tenga bajeti vizuri ili uweze kulipa mkopo wako bila shida."
            
        res["sacco_health_status"] = health_status
        res["nudge_message"] = nudge if "swahili" in res.get("justification", "").lower() or "sheng" in res.get("justification", "").lower() else res.get("justification", nudge)
        res["confidence"] = res.get("confidence", 0.5)
        res["framework"] = "SASRA Sacco Regulations & Financial Literacy Guidelines"
        return res