File size: 8,852 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
"""
Finance Knowledge Graph for Senti AI.
Structured relationships between financial entities.

What KAG knows that RAG does not:
  RAG: "What does section 12 of the Finance Act say?"
  KAG: "This invoice uses account 2020. 2020 is linked
        to VAT Payable. VAT Payable links to KRA obligation.
        KRA obligation has a 20th-of-month deadline rule.
        Therefore: this invoice triggers a tax obligation."

KAG handles multi-hop reasoning like:
  "Does this transaction affect my PAYE?"
  Step 1: transaction β†’ expense category
  Step 2: expense category β†’ account code
  Step 3: account code β†’ is_payroll_related?
  Step 4: is_payroll_related β†’ PAYE implications

Pre-built graphs:
  1. Chart of Accounts graph
  2. Tax rule dependency graph
  3. Kenyan entity hierarchy graph (regulatory bodies)
"""

import networkx as nx
from typing import Optional, List

class FinanceKnowledgeGraph:

  def __init__(self):
    self.coa_graph    = self._build_coa_graph()
    self.tax_graph    = self._build_tax_graph()
    self.entity_graph = self._build_entity_graph()

  def _build_coa_graph(self) -> nx.DiGraph:
    """
    Chart of Accounts relationship graph.
    Nodes: account codes
    Edges: parent-child + related-to relationships
    """
    G = nx.DiGraph()

    # Account nodes with metadata
    accounts = [
      ("1010", {"name": "Cash at Hand",            "type": "asset",     "liquidity": "high"}),
      ("1020", {"name": "M-Pesa Wallet",            "type": "asset",     "liquidity": "high"}),
      ("1040", {"name": "Accounts Receivable",      "type": "asset",     "liquidity": "medium"}),
      ("1050", {"name": "Inventory",                "type": "asset",     "liquidity": "medium"}),
      ("2010", {"name": "Accounts Payable",         "type": "liability", "urgency": "medium"}),
      ("2020", {"name": "VAT Payable",              "type": "liability", "urgency": "high", "tax": "VAT"}),
      ("2030", {"name": "PAYE Payable",             "type": "liability", "urgency": "high", "tax": "PAYE"}),
      ("2031", {"name": "NSSF Payable",             "type": "liability", "urgency": "high", "tax": "NSSF"}),
      ("2040", {"name": "TOT Payable",              "type": "liability", "urgency": "high", "tax": "TOT"}),
      ("4010", {"name": "Sales Revenue",            "type": "revenue",   "kra_relevant": True}),
      ("5020", {"name": "Purchases",                "type": "expense",   "cogs": True}),
      ("6010", {"name": "Staff Salaries",           "type": "expense",   "payroll": True}),
    ]

    for code, attrs in accounts:
      G.add_node(code, **attrs)

    # Relationships
    edges = [
      # Revenue triggers TOT obligation
      ("4010", "2040", {"relation": "triggers_obligation", "condition": "revenue > 500000 annually"}),
      # Salary expense triggers PAYE
      ("6010", "2030", {"relation": "triggers_obligation", "condition": "always"}),
      ("6010", "2031", {"relation": "triggers_obligation", "condition": "always"}),
      # VAT on purchases creates input VAT
      ("5020", "2020", {"relation": "input_vat_eligible",  "condition": "if vat registered"}),
      # Cash and M-Pesa are liquidity sources
      ("1010", "1020", {"relation": "liquid_substitute"}),
      # Receivables convert to cash
      ("1040", "1010", {"relation": "converts_to",         "condition": "on collection"}),
    ]

    for src, dst, attrs in edges:
      G.add_edge(src, dst, **attrs)

    return G

  def _build_tax_graph(self) -> nx.DiGraph:
    """
    Kenya tax rule dependency graph.
    Nodes: tax types, thresholds, obligations.
    """
    G = nx.DiGraph()

    G.add_nodes_from([
      ("PAYE",      {"rate": "10-35%", "due": "20th", "authority": "KRA", "filing": "iTax"}),
      ("TOT",       {"rate": "3%",     "due": "20th", "authority": "KRA",
                     "threshold_min_annual": 500000, "threshold_max_annual": 25000000}),
      ("VAT",       {"rate": "16%",    "due": "20th", "authority": "KRA",
                     "threshold_annual": 5000000}),
      ("NSSF",      {"rate": "6%",     "due": "15th", "authority": "NSSF",
                     "employer_match": True}),
      ("SHA",       {"rate": "2.75%",  "due": "15th", "authority": "SHA"}),
      ("HOUSING",   {"rate": "1.5%",   "due": "9th",  "authority": "NHCF"}),
      ("CORP_TAX",  {"rate": "30%",    "due": "annual","authority": "KRA"}),
    ])

    G.add_edges_from([
      ("PAYE",   "KRA",  {"action": "file_and_pay"}),
      ("TOT",    "KRA",  {"action": "file_and_pay", "paybill": "572572"}),
      ("VAT",    "KRA",  {"action": "file_and_pay"}),
      ("NSSF",   "NSSF", {"action": "remit"}),
      ("SHA",    "SHA",  {"action": "remit"}),
      ("HOUSING","NHCF", {"action": "remit"}),
    ])

    return G

  def _build_entity_graph(self) -> nx.DiGraph:
    """
    Kenya financial regulatory entity graph.
    Who regulates whom. Where to file what.
    """
    G = nx.DiGraph()

    G.add_nodes_from([
      ("KRA",    {"full_name": "Kenya Revenue Authority",   "website": "kra.go.ke",    "portal": "itax.kra.go.ke"}),
      ("CBK",    {"full_name": "Central Bank of Kenya",     "website": "centralbank.go.ke"}),
      ("CMA",    {"full_name": "Capital Markets Authority", "website": "cma.or.ke"}),
      ("SASRA",  {"full_name": "SACCO Societies Regulatory Authority", "website": "sasra.go.ke"}),
      ("NSSF",   {"full_name": "National Social Security Fund"}),
      ("SHA",    {"full_name": "Social Health Authority"}),
      ("NHCF",   {"full_name": "National Housing Corp Fund"}),
      ("NSE",    {"full_name": "Nairobi Securities Exchange","website": "nse.co.ke"}),
    ])

    G.add_edges_from([
      ("CBK",   "BANKS",  {"relation": "regulates"}),
      ("CMA",   "FUNDS",  {"relation": "regulates"}),
      ("SASRA", "SACCOS", {"relation": "regulates"}),
      ("KRA",   "TAXPAYERS", {"relation": "collects_from"}),
    ])

    return G

  def query(
    self,
    start_node: str,
    query_type: str,
    graph_name: str = "coa"
  ) -> dict:
    """
    Answer a structured question using graph traversal.
    query_type: obligations|dependencies|path_to|related
    """

    graph = {
      "coa":    self.coa_graph,
      "tax":    self.tax_graph,
      "entity": self.entity_graph,
    }.get(graph_name, self.coa_graph)

    if start_node not in graph:
      return {"found": False, "node": start_node}

    node_data = graph.nodes[start_node]
    result = {"node": start_node, "data": node_data}

    if query_type == "obligations":
      # Find what tax obligations this account triggers
      obligations = []
      for _, target, edge_data in graph.out_edges(start_node, data=True):
        if edge_data.get("relation") == "triggers_obligation":
          target_data = graph.nodes.get(target, {})
          obligations.append({
            "obligation":  target,
            "name":        target_data.get("name", target),
            "tax_type":    target_data.get("tax"),
            "condition":   edge_data.get("condition"),
          })
      result["obligations"] = obligations

    elif query_type == "related":
      # Direct neighbors
      neighbors = list(graph.neighbors(start_node))
      result["related"] = [
        {"code": n, **graph.nodes[n]}
        for n in neighbors
      ]

    elif query_type == "path_to":
      # Placeholder for multi-hop path finding
      result["paths"] = []

    return result

  def is_tax_relevant(self, account_code: str) -> dict:
    """Quick check: does this account have tax implications?"""

    if account_code not in self.coa_graph:
      return {"tax_relevant": False}

    obligations = self.query(account_code, "obligations")
    has_obligations = len(obligations.get("obligations", [])) > 0

    node = self.coa_graph.nodes.get(account_code, {})
    is_tax_account = "tax" in node

    return {
      "tax_relevant":  has_obligations or is_tax_account,
      "obligations":   obligations.get("obligations", []),
      "account_type":  node.get("type"),
    }

  def explain_transaction(
    self, amount: float, account_code: str
  ) -> str:
    """
    Explain what a transaction means in KAG terms.
    Used to enrich LLM context for complex queries.
    """

    tax_check = self.is_tax_relevant(account_code)
    node = self.coa_graph.nodes.get(account_code, {})

    lines = [f"Account {account_code} ({node.get('name', 'Unknown')})"]

    if tax_check["tax_relevant"]:
      for obl in tax_check["obligations"]:
        tax = obl.get("tax_type") or obl.get("name")
        if tax:
          lines.append(f"  β†’ Triggers {tax} obligation")

    if node.get("payroll"):
      lines.append("  β†’ Payroll-related: PAYE, NSSF, SHA, Housing Levy apply")

    if node.get("kra_relevant"):
      lines.append(f"  β†’ KES {amount:,.0f} is KRA-reportable revenue")

    return "\n".join(lines)

finance_kg = FinanceKnowledgeGraph()