GitHub Action commited on
Commit
9ebcdd4
·
1 Parent(s): 656e173

sync: feat: integrate protected BrgyLink chatbot AI service

Browse files
app.py CHANGED
@@ -13,6 +13,7 @@ from werkzeug.exceptions import RequestEntityTooLarge
13
  from utils.anticheat import AntiCheatEngine, AntiCheatIndeterminate, AntiCheatUnavailable
14
  from utils.predictor import Predictor, PredictorUnavailable
15
  from utils.verdict import get_verdict
 
16
 
17
 
18
  load_dotenv()
@@ -28,6 +29,7 @@ AI_SERVICE_TOKEN = os.getenv('AI_SERVICE_TOKEN', '')
28
  logger.info('Loading the Mission17 TensorFlow CNN verification service.')
29
  anticheat = AntiCheatEngine()
30
  predictor = Predictor()
 
31
 
32
 
33
  def allowed_file(filename):
@@ -166,5 +168,21 @@ def predict():
166
  return jsonify({'error': 'Processing failed'}), 500
167
 
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  if __name__ == '__main__':
170
  app.run(host='0.0.0.0', port=7860, debug=False)
 
13
  from utils.anticheat import AntiCheatEngine, AntiCheatIndeterminate, AntiCheatUnavailable
14
  from utils.predictor import Predictor, PredictorUnavailable
15
  from utils.verdict import get_verdict
16
+ from chatbot_runtime.service import ChatService
17
 
18
 
19
  load_dotenv()
 
29
  logger.info('Loading the Mission17 TensorFlow CNN verification service.')
30
  anticheat = AntiCheatEngine()
31
  predictor = Predictor()
32
+ chatbot = ChatService()
33
 
34
 
35
  def allowed_file(filename):
 
168
  return jsonify({'error': 'Processing failed'}), 500
169
 
170
 
171
+ @app.route('/chat', methods=['POST'])
172
+ def chat():
173
+ """Server-to-server BrgyLink AI chatbot endpoint.
174
+
175
+ It deliberately accepts only a short message and an optional opaque session
176
+ identifier. The mobile client never calls this Space directly and resident
177
+ history or account data is not sent to Hugging Face.
178
+ """
179
+ auth_error = require_backend_service_token()
180
+ if auth_error:
181
+ return auth_error
182
+
183
+ status_code, payload = chatbot.respond(request.get_json(silent=True))
184
+ return jsonify(payload), status_code
185
+
186
+
187
  if __name__ == '__main__':
188
  app.run(host='0.0.0.0', port=7860, debug=False)
chatbot_runtime/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Bundled, safety-constrained BrgyLink chatbot runtime.
2
+
3
+ The source-of-truth project remains the separate BrgyLink-AI-Chatbot
4
+ repository. This package contains the minimal offline runtime required by the
5
+ shared Hugging Face AI service.
6
+ """
chatbot_runtime/data/intents_brgylink_curated.json ADDED
The diff for this file is too large to render. See raw diff
 
chatbot_runtime/knowledge_base.json ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "metadata": {
3
+ "barangay_name": "Barangay Bagong Pag-asa, San Jacinto",
4
+ "status": "prototype_draft",
5
+ "last_updated": "2026-09-21",
6
+ "important": "Do not publish entries marked needs_staff_verification until an authorized barangay representative has confirmed them.",
7
+ "languages": [
8
+ "English",
9
+ "Tagalog",
10
+ "Ilocano",
11
+ "Pangasinan"
12
+ ],
13
+ "municipality": "San Jacinto, Pangasinan"
14
+ },
15
+ "services": [
16
+ {
17
+ "intent": "clearance",
18
+ "title": "Barangay Clearance",
19
+ "needs_staff_verification": true,
20
+ "requirements": [
21
+ "Submit through BrgyLink Document Requests",
22
+ "Enter the request purpose",
23
+ "Attach a photo of a valid ID"
24
+ ],
25
+ "fee_php": null,
26
+ "processing_time": null,
27
+ "answer": {
28
+ "english": "To request a Barangay Clearance, open Document Requests in BrgyLink, select Barangay Clearance, enter your purpose, attach a clear photo of your valid ID, and submit. You can track progress in the app. For fees and pickup details, please inquire at the barangay office.",
29
+ "tagalog": "Para humiling ng Barangay Clearance, buksan ang Document Requests sa BrgyLink, piliin ang Barangay Clearance, ilagay ang layunin, mag-attach ng malinaw na photo ng valid ID, at i-submit. Masusubaybayan ang progreso sa app. Para sa mga bayarin at pickup, magtanong sa opisina ng barangay.",
30
+ "ilocano": "Tapno agkiddaw iti Barangay Clearance, lukatan ti Document Requests iti BrgyLink, piliem ti Barangay Clearance, ikabil ti panggep, i-attach ti nalawag a ladawan ti valid ID, ken i-submit. Masubaybayam ti progreso iti app. Para kadagiti bayad ken pickup, saludsoden iti opisina ti barangay.",
31
+ "pangasinan": "Pian mangala na Barangay Clearance, lukatan so Document Requests ed BrgyLink, piliyen so Barangay Clearance, ilagay so gagala, i-attach so malinew ya photo na valid ID, tan i-submit. Nayarin bantayan so progreso ed app. Para ed saray bayar tan pickup, mantepet ed opisina na barangay."
32
+ },
33
+ "verified": false,
34
+ "verified_by": null,
35
+ "verified_at": null,
36
+ "expires_at": null,
37
+ "source": null,
38
+ "last_reviewed_at": null,
39
+ "review_notes": "",
40
+ "content_type": "barangay_fact",
41
+ "requires_verification": true
42
+ },
43
+ {
44
+ "intent": "indigency",
45
+ "title": "Certificate of Indigency",
46
+ "needs_staff_verification": true,
47
+ "requirements": [
48
+ "Submit through BrgyLink Document Requests",
49
+ "Enter the request purpose",
50
+ "Attach a photo of a valid ID"
51
+ ],
52
+ "fee_php": null,
53
+ "processing_time": null,
54
+ "answer": {
55
+ "english": "To request a Certificate of Indigency, open Document Requests in BrgyLink, choose Certificate of Indigency, state your purpose (e.g., medical or scholarship assistance), attach a valid ID, and submit. For eligibility requirements and processing details, please inquire at the barangay office.",
56
+ "tagalog": "Para humiling ng Certificate of Indigency, buksan ang Document Requests sa BrgyLink, piliin ang Certificate of Indigency, ilagay ang layunin (hal., medikal o scholarship), mag-attach ng valid ID, at i-submit. Para sa mga kinakailangan at detalye ng proseso, magtanong sa opisina ng barangay.",
57
+ "ilocano": "Tapno agkiddaw iti Certificate of Indigency, lukatan ti Document Requests iti BrgyLink, piliem ti Certificate of Indigency, ibaga ti panggep (kas pagarigan, medikal wenno scholarship), i-attach ti valid ID, ken i-submit. Para kadagiti kasapulan ken detalye ti proseso, saludsoden iti opisina ti barangay.",
58
+ "pangasinan": "Pian mangala na Certificate of Indigency, lukatan so Document Requests ed BrgyLink, piliyen so Certificate of Indigency, ilagay so gagala (singa medikal odino scholarship), i-attach so valid ID, tan i-submit. Para ed saray kailangan tan detalye na proseso, mantepet ed opisina na barangay."
59
+ },
60
+ "verified": false,
61
+ "verified_by": null,
62
+ "verified_at": null,
63
+ "expires_at": null,
64
+ "source": null,
65
+ "last_reviewed_at": null,
66
+ "review_notes": "",
67
+ "content_type": "barangay_fact",
68
+ "requires_verification": true
69
+ },
70
+ {
71
+ "intent": "residency",
72
+ "title": "Certificate of Residency",
73
+ "needs_staff_verification": true,
74
+ "requirements": [
75
+ "Submit through BrgyLink Document Requests",
76
+ "Enter the request purpose",
77
+ "Attach a photo of a valid ID"
78
+ ],
79
+ "fee_php": null,
80
+ "processing_time": null,
81
+ "answer": {
82
+ "english": "To request a Certificate of Residency, open Document Requests in BrgyLink, select Certificate of Residency, specify your purpose, attach a valid ID, and submit. For requirements and processing details, please inquire at the barangay office.",
83
+ "tagalog": "Para humiling ng Certificate of Residency, buksan ang Document Requests sa BrgyLink, piliin ang Certificate of Residency, ilagay ang layunin, mag-attach ng valid ID, at i-submit. Para sa mga kinakailangan at detalye ng proseso, magtanong sa opisina ng barangay.",
84
+ "ilocano": "Tapno agkiddaw iti Certificate of Residency, lukatan ti Document Requests iti BrgyLink, piliem ti Certificate of Residency, ibaga ti panggep, i-attach ti valid ID, ken i-submit. Para kadagiti kasapulan ken detalye ti proseso, saludsoden iti opisina ti barangay.",
85
+ "pangasinan": "Pian mangala na Certificate of Residency, lukatan so Document Requests ed BrgyLink, piliyen so Certificate of Residency, ilagay so gagala, i-attach so valid ID, tan i-submit. Para ed saray kailangan tan detalye na proseso, mantepet ed opisina na barangay."
86
+ },
87
+ "verified": false,
88
+ "verified_by": null,
89
+ "verified_at": null,
90
+ "expires_at": null,
91
+ "source": null,
92
+ "last_reviewed_at": null,
93
+ "review_notes": "",
94
+ "content_type": "barangay_fact",
95
+ "requires_verification": true
96
+ },
97
+ {
98
+ "intent": "business_permit",
99
+ "title": "Barangay Business Clearance",
100
+ "needs_staff_verification": true,
101
+ "requirements": [
102
+ "Submit through BrgyLink Document Requests",
103
+ "Enter the request purpose",
104
+ "Attach a photo of a valid ID"
105
+ ],
106
+ "fee_php": null,
107
+ "processing_time": null,
108
+ "answer": {
109
+ "english": "To apply for a Barangay Business Clearance, open Document Requests in BrgyLink, select Business Clearance, and submit your application. For specific requirements, fees, and processing details, please inquire at the barangay office.",
110
+ "tagalog": "Para mag-apply ng Barangay Business Clearance, buksan ang Document Requests sa BrgyLink, piliin ang Business Clearance, at i-submit ang application. Para sa mga kinakailangan, bayarin, at detalye ng proseso, magtanong sa opisina ng barangay.",
111
+ "ilocano": "Tapno ag-apply iti Barangay Business Clearance, lukatan ti Document Requests iti BrgyLink, piliem ti Business Clearance, ken i-submit ti aplikasion. Para kadagiti kasapulan, bayad, ken detalye ti proseso, saludsoden iti opisina ti barangay.",
112
+ "pangasinan": "Pian man-apply na Barangay Business Clearance, lukatan so Document Requests ed BrgyLink, piliyen so Business Clearance, tan i-submit so aplikasyon. Para ed saray kailangan, bayar, tan detalye na proseso, mantepet ed opisina na barangay."
113
+ },
114
+ "verified": false,
115
+ "verified_by": null,
116
+ "verified_at": null,
117
+ "expires_at": null,
118
+ "source": null,
119
+ "last_reviewed_at": null,
120
+ "review_notes": "",
121
+ "content_type": "barangay_fact",
122
+ "requires_verification": true
123
+ },
124
+ {
125
+ "intent": "office_hours",
126
+ "title": "Office Hours and Location",
127
+ "needs_staff_verification": true,
128
+ "office_hours": null,
129
+ "office_address": null,
130
+ "contact": null,
131
+ "answer": {
132
+ "english": "I cannot confirm a physical address or live office schedule. For the location of Barangay Bagong Pag-asa, please check official BrgyLink Announcements or inquire directly with the barangay office. Use the same channels to confirm office hours before visiting.",
133
+ "tagalog": "Hindi ko makukumpirma ang pisikal na address o kasalukuyang office schedule. Para sa lokasyon ng Barangay Bagong Pag-asa, tingnan ang official Announcements sa BrgyLink o magtanong mismo sa opisina ng barangay. Doon din kumpirmahin ang oras ng opisina bago bumisita.",
134
+ "ilocano": "Saan a makumpirma ti chatbot ti pisikal a address wenno agdama nga iskediul ti opisina. Para iti lokasion ti Barangay Bagong Pag-asa, kitaem ti opisial nga Announcements iti BrgyLink wenno saludsodem mismo iti opisina ti barangay. Ikumpirmam met ti oras ti opisina sakbay a bumisita.",
135
+ "pangasinan": "Ag na-confirm na chatbot so pisikal ya address odino kasalukuyan ya schedule na opisina. Para ed lokasyon na Barangay Bagong Pag-asa, nengnengen so opisyal ya Announcements ed BrgyLink odino mantepet mismo ed opisina na barangay. I-confirm met so oras na opisina antes bumisita."
136
+ },
137
+ "verified": false,
138
+ "verified_by": null,
139
+ "verified_at": null,
140
+ "expires_at": null,
141
+ "source": null,
142
+ "last_reviewed_at": null,
143
+ "review_notes": "",
144
+ "content_type": "barangay_fact",
145
+ "requires_verification": true
146
+ },
147
+ {
148
+ "intent": "fees",
149
+ "title": "Service Fees",
150
+ "needs_staff_verification": true,
151
+ "answer": {
152
+ "english": "Service fees for barangay certificates and clearances vary. For the current fee schedule, please inquire at the barangay office or check the Announcements section in BrgyLink.",
153
+ "tagalog": "Ang mga bayarin sa sertipiko at clearance ng barangay ay iba-iba. Para sa kasalukuyang listahan ng bayarin, magtanong sa opisina ng barangay o tingnan ang Announcements sa BrgyLink.",
154
+ "ilocano": "Dagiti bayad kadagiti sertipiko ken clearance ti barangay ket agduduma. Para iti agdama a listaan dagiti bayad, saludsoden iti opisina ti barangay wenno kitaen ti Announcements iti BrgyLink.",
155
+ "pangasinan": "Saray bayar ed sertipiko tan clearance na barangay et nanduruma. Para ed listaan na saray bayar natan, mantepet ed opisina na barangay odino nengnengen so Announcements ed BrgyLink."
156
+ },
157
+ "verified": false,
158
+ "verified_by": null,
159
+ "verified_at": null,
160
+ "expires_at": null,
161
+ "source": null,
162
+ "last_reviewed_at": null,
163
+ "review_notes": "",
164
+ "content_type": "barangay_fact",
165
+ "requires_verification": true
166
+ },
167
+ {
168
+ "intent": "report_incident",
169
+ "title": "Complaint or Blotter",
170
+ "needs_staff_verification": true,
171
+ "answer": {
172
+ "english": "To file an incident report, open Blotter Reports in BrgyLink, choose File New Report, select the incident category, enter the details and location, and submit. You can track your report status in the app. For emergencies, call 911.",
173
+ "tagalog": "Para mag-file ng incident report, buksan ang Blotter Reports sa BrgyLink, piliin ang File New Report, piliin ang kategorya, ilagay ang mga detalye at lokasyon, at i-submit. Masusubaybayan ang status ng report sa app. Para sa emergency, tumawag sa 911.",
174
+ "ilocano": "Tapno ag-file iti incident report, lukatan ti Blotter Reports iti BrgyLink, piliem ti File New Report, piliem ti kategorya, ikabil dagiti detalye ken lokasion, ken i-submit. Masubaybayam ti status ti report iti app. Para iti emergency, umawag iti 911.",
175
+ "pangasinan": "Pian man-file na incident report, lukatan so Blotter Reports ed BrgyLink, piliyen so File New Report, piliyen so kategorya, ilagay so saray detalye tan lokasyon, tan i-submit. Nayarin bantayan so status na report ed app. Para ed emergency, tawagan so 911."
176
+ },
177
+ "verified": false,
178
+ "verified_by": null,
179
+ "verified_at": null,
180
+ "expires_at": null,
181
+ "source": null,
182
+ "last_reviewed_at": null,
183
+ "review_notes": "",
184
+ "content_type": "barangay_fact",
185
+ "requires_verification": true
186
+ },
187
+ {
188
+ "intent": "emergency",
189
+ "title": "Emergency",
190
+ "needs_staff_verification": true,
191
+ "emergency_contacts": {
192
+ "national_emergency": "911",
193
+ "barangay_tanod": null,
194
+ "fire": null,
195
+ "ambulance": null
196
+ },
197
+ "answer": {
198
+ "english": "If there is immediate danger to life or property, call 911 immediately. Do not wait for a chatbot reply. The chatbot cannot dispatch help or contact emergency services for you.",
199
+ "tagalog": "Kung may agarang panganib sa buhay o ari-arian, tumawag agad sa 911. Huwag maghintay ng sagot ng chatbot. Hindi kayang magpadala ng tulong o tumawag sa emergency services para sa iyo ang chatbot.",
200
+ "ilocano": "No adda dagus a peggad iti biag wenno sanikua, umawag a dagus iti 911. Saan nga urayen ti sungbat ti chatbot. Saan a makapatulod ti chatbot iti tulong wenno umawag kadagiti emergency services para kenka.",
201
+ "pangasinan": "No walay tampol ya peligro ed bilay odino kayarian, tawagan ya tampol so 911. Ag-antayan so ebat na chatbot. Ag makapatulod na tulong so chatbot odino ontawag ed emergency services para ed sika."
202
+ },
203
+ "verified": false,
204
+ "verified_by": null,
205
+ "verified_at": null,
206
+ "expires_at": null,
207
+ "source": null,
208
+ "last_reviewed_at": null,
209
+ "review_notes": "",
210
+ "content_type": "barangay_fact",
211
+ "requires_verification": true
212
+ },
213
+ {
214
+ "intent": "garbage",
215
+ "title": "Garbage Collection",
216
+ "needs_staff_verification": true,
217
+ "schedule": null,
218
+ "answer": {
219
+ "english": "Garbage collection in Barangay Bagong Pag-asa follows a designated schedule per purok. For your collection day and segregation guidelines, please inquire at the barangay office or check the Announcements section in BrgyLink.",
220
+ "tagalog": "Ang hakot ng basura sa Barangay Bagong Pag-asa ay may takdang schedule bawat purok. Para sa araw ng koleksyon at mga panuntunan sa segregasyon, magtanong sa opisina ng barangay o tingnan ang Announcements sa BrgyLink.",
221
+ "ilocano": "Ti panagala ti basura iti Barangay Bagong Pag-asa ket addaan iti naikeddeng nga iskediul tunggal purok. Para iti aldaw ti koleksion ken paglintegan ti segregasion, saludsoden iti opisina ti barangay wenno kitaen ti Announcements iti BrgyLink.",
222
+ "pangasinan": "Say panangala na basura ed Barangay Bagong Pag-asa et walay niganan ya schedule ed balang purok. Para ed agew na koleksyon tan saray totontonen ed segregasyon, mantepet ed opisina na barangay odino nengnengen so Announcements ed BrgyLink."
223
+ },
224
+ "verified": false,
225
+ "verified_by": null,
226
+ "verified_at": null,
227
+ "expires_at": null,
228
+ "source": null,
229
+ "last_reviewed_at": null,
230
+ "review_notes": "",
231
+ "content_type": "barangay_fact",
232
+ "requires_verification": true
233
+ },
234
+ {
235
+ "intent": "good_moral",
236
+ "title": "Certificate of Good Moral Character",
237
+ "needs_staff_verification": true,
238
+ "requirements": [
239
+ "Submit through BrgyLink Document Requests",
240
+ "Enter the request purpose",
241
+ "Attach a photo of a valid ID"
242
+ ],
243
+ "answer": {
244
+ "english": "To request a Certificate of Good Moral Character, open Document Requests in BrgyLink, select Good Moral Character, state your purpose, attach a valid ID, and submit. For requirements and processing details, please inquire at the barangay office.",
245
+ "tagalog": "Para humiling ng Certificate of Good Moral Character, buksan ang Document Requests sa BrgyLink, piliin ang Good Moral Character, ilagay ang layunin, mag-attach ng valid ID, at i-submit. Para sa mga kinakailangan at detalye ng proseso, magtanong sa opisina ng barangay.",
246
+ "ilocano": "Tapno agkiddaw iti Certificate of Good Moral Character, lukatan ti Document Requests iti BrgyLink, piliem ti Good Moral Character, ibaga ti panggep, i-attach ti valid ID, ken i-submit. Para kadagiti kasapulan ken detalye ti proseso, saludsoden iti opisina ti barangay.",
247
+ "pangasinan": "Pian mangala na Certificate of Good Moral Character, lukatan so Document Requests ed BrgyLink, piliyen so Good Moral Character, ilagay so gagala, i-attach so valid ID, tan i-submit. Para ed saray kailangan tan detalye na proseso, mantepet ed opisina na barangay."
248
+ },
249
+ "verified": false,
250
+ "verified_by": null,
251
+ "verified_at": null,
252
+ "expires_at": null,
253
+ "source": null,
254
+ "last_reviewed_at": null,
255
+ "review_notes": "",
256
+ "content_type": "barangay_fact",
257
+ "requires_verification": true
258
+ },
259
+ {
260
+ "intent": "barangay_id",
261
+ "title": "Barangay ID",
262
+ "needs_staff_verification": true,
263
+ "requirements": [
264
+ "Submit through BrgyLink Document Requests",
265
+ "Enter the request purpose",
266
+ "Attach a photo of a valid ID when requested"
267
+ ],
268
+ "answer": {
269
+ "english": "To apply for a Barangay ID, open Document Requests in BrgyLink, select Barangay ID, provide your resident details, and submit. For specific requirements and processing time, please inquire at the barangay office.",
270
+ "tagalog": "Para mag-apply ng Barangay ID, buksan ang Document Requests sa BrgyLink, piliin ang Barangay ID, ilagay ang iyong mga detalye bilang residente, at i-submit. Para sa mga kinakailangan at oras ng proseso, magtanong sa opisina ng barangay.",
271
+ "ilocano": "Tapno ag-apply iti Barangay ID, lukatan ti Document Requests iti BrgyLink, piliem ti Barangay ID, ikabil dagiti detalye a kas residente, ken i-submit. Para kadagiti kasapulan ken oras ti proseso, saludsoden iti opisina ti barangay.",
272
+ "pangasinan": "Pian man-apply na Barangay ID, lukatan so Document Requests ed BrgyLink, piliyen so Barangay ID, ilagay so saray detalye bilang residente, tan i-submit. Para ed saray kailangan tan oras na proseso, mantepet ed opisina na barangay."
273
+ },
274
+ "verified": false,
275
+ "verified_by": null,
276
+ "verified_at": null,
277
+ "expires_at": null,
278
+ "source": null,
279
+ "last_reviewed_at": null,
280
+ "review_notes": "",
281
+ "content_type": "barangay_fact",
282
+ "requires_verification": true
283
+ },
284
+ {
285
+ "intent": "document_status",
286
+ "title": "Document Request Status",
287
+ "needs_staff_verification": false,
288
+ "statuses": [
289
+ "Pending",
290
+ "Processing",
291
+ "Ready for Pickup",
292
+ "Completed",
293
+ "Rejected"
294
+ ],
295
+ "answer": {
296
+ "english": "To check the status of your document request, open Document Requests in BrgyLink. Your request will show as Pending, Processing, Ready for Pickup (available at the barangay hall), Completed, or Rejected with reason noted.",
297
+ "tagalog": "Upang suriin ang status ng iyong hiniling na dokumento, buksan ang 'Document Requests' sa BrgyLink. Dito makikita kung ito ay: Pending (nakabinbin), Processing (inaasikaso), Ready for Pickup (maaari nang kunin sa barangay hall), Completed, o Rejected. May kaukulang dahilan din kung sakaling ma-reject ang request.",
298
+ "ilocano": "Tapno makita ti kasasaad ti kineddawmo a dokumento, lukatan ti 'Document Requests' iti BrgyLink. Dita makitam no: Pending, Processing, Ready for Pickup (mabalinen nga alaen iti barangay hall), Completed, wenno Rejected.",
299
+ "pangasinan": "Pian nengnengen so status na kineddeng mon dokumento, lukatan so 'Document Requests' diad BrgyLink. Ditan nanengneng no: Pending, Processing, Ready for Pickup (nayari lay alaen ed barangay hall), Completed, odino Rejected."
300
+ },
301
+ "verified": false,
302
+ "verified_by": null,
303
+ "verified_at": null,
304
+ "expires_at": null,
305
+ "source": null,
306
+ "last_reviewed_at": null,
307
+ "review_notes": "",
308
+ "content_type": "barangay_fact",
309
+ "requires_verification": true
310
+ },
311
+ {
312
+ "intent": "registration",
313
+ "title": "Resident Registration",
314
+ "needs_staff_verification": false,
315
+ "steps": [
316
+ "Tap Create Account",
317
+ "Enter full name, email, username, phone number, and home address",
318
+ "Complete email verification"
319
+ ],
320
+ "answer": {
321
+ "english": "To register, begin with your email in Step 1. Enter the six-digit code sent to that email before continuing. Then complete your resident details, upload clear photos of the required government-issued ID, create your password in the final step, accept the Privacy Notice and Terms of Use, and submit the registration for barangay review. Residents must be at least 15 years old. Never share your verification code.",
322
+ "tagalog": "Para mag-register, magsimula sa email sa Step 1. Ilagay ang anim na digit na code na ipinadala sa email bago magpatuloy. Kumpletuhin ang resident details, mag-upload ng malinaw na larawan ng hinihinging government-issued ID, gumawa ng password sa huling step, tanggapin ang Privacy Notice at Terms of Use, at isumite ang registration para sa barangay review. Kailangan ay hindi bababa sa 15 taong gulang. Huwag ibahagi ang verification code.",
323
+ "ilocano": "Tapno agparehistro, mangrugi iti email iti Step 1 ken ikabil ti innem a digit a code sakbay a mangtuloy. Kumpletoem dagiti resident details ken mang-upload iti nalawag a ladawan ti government-issued ID. Aramidem ti password iti maudi a step, awatem ti Privacy Notice ken Terms of Use, ken isumitem para iti barangay review. Masapul nga agtawen iti 15 wenno nangatngato. Saan nga ibagbaga ti verification code.",
324
+ "pangasinan": "Pian mag-register, manggapo ed email ed Step 1 tan isulat so anem ya digit a code antes ontuloy. Kumpletoen so resident details tan mang-upload na malinew ya litrato na government-issued ID. Gawaen so password ed unor ya step, awaten so Privacy Notice tan Terms of Use, tan isumite para ed barangay review. Kasapulan ya 15 taon odino mas matanda. Agmon iter so verification code."
325
+ },
326
+ "verified": false,
327
+ "verified_by": null,
328
+ "verified_at": null,
329
+ "expires_at": null,
330
+ "source": null,
331
+ "last_reviewed_at": null,
332
+ "review_notes": "",
333
+ "content_type": "barangay_fact",
334
+ "requires_verification": true,
335
+ "answer_variants": {
336
+ "id": {
337
+ "english": "Residents must be at least 15 to register. In the final signup step, select your ID type and take a clear photo: all details must be readable, with no glare, blur, or cut-off edges. Upload the front and back when requested; Passport uses the information page. If you do not have an accepted ID, ask the barangay office what to do. The chatbot cannot approve an ID or bypass the requirement.",
338
+ "tagalog": "Kailangan ay hindi bababa sa 15 taong gulang para mag-register. Sa huling signup step, piliin ang ID type at kumuha ng malinaw na larawan: nababasa ang detalye, walang glare o blur, at hindi putol ang gilid. I-upload ang front at back kung hinihingi; para sa Passport, information page ang kailangan. Kung walang tinatanggap na ID, magtanong sa opisina ng barangay. Hindi kayang mag-approve ng ID o lumampas sa requirement ang chatbot.",
339
+ "ilocano": "Masapul nga agtawen iti 15 wenno nangatngato tapno agparehistro. Iti maudi a signup step, piliem ti ID type ken alaem ti nalawag a ladawan: mabasa amin a detalye, awan glare wenno blur, ken saan a naputed ti igid. I-upload ti front ken back no kasapulan; ti Passport ket information page. No awan ti maawat nga ID, agsaludsod iti opisina ti barangay. Saan a maka-approve ti chatbot iti ID wenno makalabsing iti requirement.",
340
+ "pangasinan": "Kasapulan ya 15 taon odino mas matanda pian mag-register. Ed unor ya signup step, piliyen so ID type tan alaen so malinew ya litrato: nabasa so amin ya detalye, anggapo so glare odino blur, tan agputol so gilid. I-upload so front tan back no kasapulan; say Passport et information page. No anggapoy tinatanggap ya ID, mantepet ed opisina na barangay. Ag maka-approve na ID so chatbot odino makalampas ed requirement."
341
+ }
342
+ }
343
+ },
344
+ {
345
+ "intent": "account_help",
346
+ "title": "Login and Email OTP",
347
+ "needs_staff_verification": false,
348
+ "answer": {
349
+ "english": "For a registration code, check Inbox, Spam, and Junk and make sure the email was entered correctly. Wait for the resend timer in the app before requesting another code; an expired code needs a new request. Do not share the code. If your registration was submitted, wait for barangay review before signing in. For a forgotten password, use Forgot Password on the sign-in screen.",
350
+ "tagalog": "Para sa registration code, tingnan ang Inbox, Spam, at Junk at tiyaking tama ang email na inilagay. Hintayin ang resend timer sa app bago humiling ulit ng code; kung expired na ang code, humiling ng bago. Huwag ibahagi ang code. Kung naisumite na ang registration, hintayin muna ang barangay review bago mag-sign in. Para sa nakalimutang password, gamitin ang Forgot Password sa sign-in screen.",
351
+ "ilocano": "No awan ti registration code, kitaem ti Inbox, Spam, ken Junk ken siguraduem a husto ti email. Urayem ti resend timer iti app sakbay a mangkiddaw manen iti code. Saan nga ibagbaga ti code. No naipasa ti registration, urayem ti barangay review sakbay a sumrek.",
352
+ "pangasinan": "No anggapo so registration code, nengnengen so Inbox, Spam, tan Junk tan seguroen ya duga so email. Antayan so resend timer ed app antes mangikeddeng lamet na code. Agmon iter so code. No naisumite lay registration, antayan so barangay review antes sumrek."
353
+ },
354
+ "verified": false,
355
+ "verified_by": null,
356
+ "verified_at": null,
357
+ "expires_at": null,
358
+ "source": null,
359
+ "last_reviewed_at": null,
360
+ "review_notes": "",
361
+ "content_type": "barangay_fact",
362
+ "requires_verification": true,
363
+ "answer_variants": {
364
+ "password": {
365
+ "english": "On the sign-in screen, tap Forgot Password, enter the email for your account, and follow the reset instructions sent to it. Check Inbox, Spam, and Junk. Use a newly requested reset link if the old one is invalid or expired. Never send your password or reset link to the chatbot. The chatbot cannot reset your password for you.",
366
+ "tagalog": "Sa sign-in screen, i-tap ang Forgot Password, ilagay ang email ng account, at sundin ang reset instructions na ipinadala rito. Tingnan ang Inbox, Spam, at Junk. Humiling ng bagong reset link kung invalid o expired na ang luma. Huwag ipadala sa chatbot ang password o reset link. Hindi kayang i-reset ng chatbot ang password para sa iyo.",
367
+ "ilocano": "Iti sign-in screen, i-tap ti Forgot Password, ikabil ti email ti account, ken surotem ti reset instructions iti email. Kitaem ti Inbox, Spam, ken Junk. Mangikiddaw iti baro a reset link no invalid wenno expired ti daan. Saan nga ipatulod iti chatbot ti password wenno reset link. Saan a ma-reset ti chatbot ti password mo.",
368
+ "pangasinan": "Ed sign-in screen, i-tap so Forgot Password, isulat so email na account, tan suroten so reset instructions ed email. Nengnengen so Inbox, Spam, tan Junk. Mangikeddeng na balon reset link no invalid odino expired lay daan. Agmon ipatulod ed chatbot so password odino reset link. Ag na-reset na chatbot so password mo."
369
+ },
370
+ "otp": {
371
+ "english": "Check that the signup email is correct and look in Inbox, Spam, and Junk. If the code has not arrived, wait for the app's resend timer, then request a new code. Enter the latest six-digit code; request another if it is expired or invalid. You can edit an incorrect email on the verification screen. Never share your OTP. The chatbot cannot see your code or confirm delivery.",
372
+ "tagalog": "Tiyaking tama ang signup email at tingnan ang Inbox, Spam, at Junk. Kung wala pa ang code, hintayin ang resend timer sa app at humiling ng bagong code. Ilagay ang pinakabagong anim na digit na code; humiling ulit kung expired o invalid. Maaari mong itama ang email sa verification screen. Huwag ibahagi ang OTP. Hindi nakikita ng chatbot ang code at hindi nito makukumpirma ang delivery.",
373
+ "ilocano": "Siguraduem a husto ti signup email ken kitaem ti Inbox, Spam, ken Junk. No awan pay ti code, urayem ti resend timer iti app ken mangkiddaw iti baro a code. Ikabil ti kabaruan nga innem a digit a code; mangkiddaw manen no expired wenno invalid. Mabalin a baliwan ti email iti verification screen. Saan nga ibagbaga ti OTP. Saan a makita ti chatbot ti code wenno makumpirma ti delivery.",
374
+ "pangasinan": "Seguroen ya duga so signup email tan nengnengen so Inbox, Spam, tan Junk. No anggapo ni so code, antayan so resend timer ed app tan mangikeddeng na balon code. Isulat so pinakabalo ya anem ya digit a code; mangikeddeng lamet no expired odino invalid. Nayari ya ontan so email ed verification screen. Agmon iter so OTP. Ag nanengneng na chatbot so code odino nakumpirma so delivery."
375
+ },
376
+ "review": {
377
+ "english": "Email verification does not mean the resident account has been approved. Submitted registrations still need barangay review. If the account is rejected, read any rejection reason shown and contact the barangay office about corrections. If it is approved but you still cannot sign in, check the email and password and report the exact error without sharing credentials. The chatbot cannot see or change your approval status.",
378
+ "tagalog": "Ang email verification ay hindi pa approval ng resident account. Kailangan pa rin ng barangay review ang naisumiteng registration. Kung rejected, basahin ang ipinakitang rejection reason at magtanong sa opisina ng barangay tungkol sa pagwawasto. Kung approved pero hindi makapasok, tiyaking tama ang email at password at i-report ang eksaktong error nang hindi ibinabahagi ang credentials. Hindi nakikita o nababago ng chatbot ang approval status.",
379
+ "ilocano": "Ti email verification ket saan pay nga approval ti resident account. Kasapulan pay ti barangay review ti naipasa a registration. No rejected, basaem ti rejection reason a naipakita ken makiuman iti opisina ti barangay maipapan iti panagkorehir. No approved ngem saan a makasrek, kitaem ti email ken password ken i-report ti error nga awan ibagbaga a credentials. Saan a makita wenno baliwan ti chatbot ti approval status.",
380
+ "pangasinan": "Say email verification et agni approval na resident account. Kasapulan ni so barangay review na naisumiten registration. No rejected, basaen so rejection reason ya nanengneng tan mantepet ed opisina na barangay nipaakar ed panangkorehir. No approved balet agmakasumrek, nengnengen so email tan password tan i-report so error ya agiter na credentials. Ag nanengneng odino nabago na chatbot so approval status."
381
+ }
382
+ }
383
+ },
384
+ {
385
+ "intent": "announcements",
386
+ "title": "Barangay Announcements",
387
+ "needs_staff_verification": false,
388
+ "answer": {
389
+ "english": "For the latest community bulletins, typhoon advisories, health mission schedules, financial aid releases, and public hearings, open the Announcements screen in BrgyLink.",
390
+ "tagalog": "Para sa pinakabagong balita, bagyo o kalamidad alerts, health center schedules, pamamahagi ng ayuda, at public hearings, buksan ang 'Announcements' screen sa BrgyLink. Regular itong ina-update ng pamahalaang barangay.",
391
+ "ilocano": "Para kadagiti kabaroan a damag, pakaammo iti bagyo ken kalamidad, iskedyul ti health center, panagiwaras ti ayuda, ken public hearings, lukatan ti 'Announcements' screen iti BrgyLink. Kanayon a mapabaro daytoy ti barangay.",
392
+ "pangasinan": "Para ed saray balon balita, pakaammo ed bagyo tan kalamidad, iskedyul na health center, panagiwaras na ayuda, tan public hearings, lukatan so 'Announcements' screen diad BrgyLink. Regular yan ina-update na barangay."
393
+ },
394
+ "verified": false,
395
+ "verified_by": null,
396
+ "verified_at": null,
397
+ "expires_at": null,
398
+ "source": null,
399
+ "last_reviewed_at": null,
400
+ "review_notes": "",
401
+ "content_type": "barangay_fact",
402
+ "requires_verification": true
403
+ },
404
+ {
405
+ "intent": "sdg_mission",
406
+ "title": "Community Initiatives and SDG Missions",
407
+ "needs_staff_verification": false,
408
+ "steps": [
409
+ "Open Community Initiatives",
410
+ "Select an active program",
411
+ "Tap Upload Proof",
412
+ "Capture live photo evidence",
413
+ "Tap Analyze & Submit"
414
+ ],
415
+ "answer": {
416
+ "english": "Open Civic Tasks & Community Events, choose an active civic task, and use Capture Live Photo to show your participation clearly. Retake unclear photos, then tap Submit Task Proof. Open Profile and Community Mission Submissions to see your own submission status and any rejection reason. Pending is not approval; the chatbot cannot see or change your result. Civic participation does not earn reward points in the current app. Follow the task instructions and do not use unrelated or reused photos.",
417
+ "tagalog": "Buksan ang Civic Tasks & Community Events, pumili ng aktibong civic task, at gamitin ang Capture Live Photo para malinaw na ipakita ang pakikilahok. Mag-retake kung malabo at i-tap ang Submit Task Proof. Sa Profile, tingnan ang Community Mission Submissions para sa sariling submission status at rejection reason kung mayroon. Hindi pa approval ang Pending; hindi nakikita o nababago ng chatbot ang resulta. Walang reward points ang civic participation sa kasalukuyang app. Sundin ang task instructions at huwag gumamit ng hindi kaugnay o lumang larawan.",
418
+ "ilocano": "Lukatan ti Civic Tasks & Community Events, piliem ti aktibo a civic task, ken usarem ti Capture Live Photo tapno nalawag ti panagpasetmo. I-retake no saan a nalawag ken i-tap ti Submit Task Proof. Iti Profile, kitaem ti Community Mission Submissions para iti bukodmo a submission status ken rejection reason no adda. Saan pay nga approval ti Pending; saan a makita wenno baliwan ti chatbot ti resulta. Awan ti reward points iti agdama nga app. Surotem dagiti task instructions ken saan nga usaren ti saan a kaugnay wenno daan a ladawan.",
419
+ "pangasinan": "Lukatan so Civic Tasks & Community Events, piliyen so aktibon civic task, tan usaren so Capture Live Photo pian malinew so pakikiba mo. I-retake no agmalinew tan i-tap so Submit Task Proof. Ed Profile, nengnengen so Community Mission Submissions para ed bukod mon submission status tan rejection reason no wala. Agni approval so Pending; ag nanengneng odino nabago na chatbot so resulta. Anggapoy reward points ed kasalukuyan ya app. Suroten so task instructions tan ag-usar na agkaugnay odino daan ya litrato."
420
+ },
421
+ "verified": false,
422
+ "verified_by": null,
423
+ "verified_at": null,
424
+ "expires_at": null,
425
+ "source": null,
426
+ "last_reviewed_at": null,
427
+ "review_notes": "",
428
+ "content_type": "barangay_fact",
429
+ "requires_verification": true
430
+ },
431
+ {
432
+ "intent": "suggestion",
433
+ "title": "Suggestions and Feedback",
434
+ "needs_staff_verification": false,
435
+ "answer": {
436
+ "english": "Do you have ideas or feedback to improve our barangay? Use the 'Feedback / Suggestions' feature in BrgyLink to send your thoughts directly to the barangay council. For formal disputes, please file a Blotter Report instead.",
437
+ "tagalog": "Mayroon ka bang mungkahi o puna para mapabuti ang ating barangay? Gamitin ang feature na 'Feedback / Suggestions' sa BrgyLink upang direktang maipadala ang iyong mensahe sa pamunuan. Para naman sa pormal na reklamo o insidente, gamitin ang 'Blotter Reports'.",
438
+ "ilocano": "Adda kadi singasingmo wenno feedback tapno mapasayaat ti barangaytayo? Usaren ti 'Feedback / Suggestions' iti BrgyLink tapno maipatulod a direkta kadagiti opisial. Para iti pormal a reklamo, usaren ti 'Blotter Reports'.",
439
+ "pangasinan": "Wala kasi suhestiyon mo odino feedback pian mas omparakep so barangay tayo? Usaren so 'Feedback / Suggestions' diad BrgyLink pian direktan nipawit ed saray opisyales. Para ed pormal ya reklamo, usaren so 'Blotter Reports'."
440
+ },
441
+ "verified": false,
442
+ "verified_by": null,
443
+ "verified_at": null,
444
+ "expires_at": null,
445
+ "source": null,
446
+ "last_reviewed_at": null,
447
+ "review_notes": "",
448
+ "content_type": "barangay_fact",
449
+ "requires_verification": true
450
+ },
451
+ {
452
+ "intent": "blotter_status",
453
+ "title": "Blotter Report Tracking",
454
+ "needs_staff_verification": false,
455
+ "answer": {
456
+ "english": "To track the status of your blotter complaint, open 'My Blotter Reports' in BrgyLink to see whether it is Under Review, Scheduled for Mediation, or Resolved. The chatbot cannot access or alter private case records.",
457
+ "tagalog": "Upang malaman ang estado ng iyong inihain na blotter, buksan ang 'My Blotter Reports' sa BrgyLink app. Dito makikita kung ang ulat ay Under Review, Scheduled for Mediation/Lupon, o Resolved na. Hindi maaaring baguhin o direktang basahin ng chatbot ang lihim na detalye ng iyong blotter.",
458
+ "ilocano": "Tapno maammuan ti kasasaad ti blottermo, lukatan ti 'My Blotter Reports' iti BrgyLink app. Makitam no daytoy ket Under Review, Para iti Lupon Hearing, wenno Naresolba. Saan a mabalin nga i-display wenno bagian ti chatbot ti personal a detalye ti kasom.",
459
+ "pangasinan": "Pian naamtaan so estado na blotter mo, lukatan so 'My Blotter Reports' diad BrgyLink app. Ditan nanengneng no say ulat et Under Review, Iskediyul parad Lupon Hearing, odino Na-ayos la. Ag nayarin direkta ya basaen na chatbot so personal ya detalye na blotter mo."
460
+ },
461
+ "verified": false,
462
+ "verified_by": null,
463
+ "verified_at": null,
464
+ "expires_at": null,
465
+ "source": null,
466
+ "last_reviewed_at": null,
467
+ "review_notes": "",
468
+ "content_type": "barangay_fact",
469
+ "requires_verification": true
470
+ },
471
+ {
472
+ "intent": "notifications",
473
+ "title": "BrgyLink Notifications",
474
+ "needs_staff_verification": false,
475
+ "answer": {
476
+ "english": "BrgyLink sends notifications for updates on your document requests, blotter reports, and urgent community advisories. Ensure notification permissions are enabled for BrgyLink in your phone settings.",
477
+ "tagalog": "Nagpapadala ang BrgyLink ng in-app at push notifications para sa updates sa iyong document request, status ng blotter, at mahahalagang anunsyo. Siguraduhing pinayagan (Enabled) ang notifications para sa BrgyLink sa settings ng iyong telepono.",
478
+ "ilocano": "Mangipatulod ti BrgyLink kadagiti notifications para kadagiti update iti document requests, blotter reports, ken anunsio. Siguraduem a naka-Enable ti notifications ti BrgyLink iti settings ti selponmo.",
479
+ "pangasinan": "Mangi-iiter so BrgyLink na notifications para ed updates ed document requests, blotter reports, tan anunsyo. Seguroen ya aka-Enable so notifications parad BrgyLink diad settings na telepono mo."
480
+ },
481
+ "verified": false,
482
+ "verified_by": null,
483
+ "verified_at": null,
484
+ "expires_at": null,
485
+ "source": null,
486
+ "last_reviewed_at": null,
487
+ "review_notes": "",
488
+ "content_type": "barangay_fact",
489
+ "requires_verification": true
490
+ },
491
+ {
492
+ "intent": "officials",
493
+ "title": "Barangay Officials",
494
+ "needs_staff_verification": true,
495
+ "answer": {
496
+ "english": "The chatbot does not have a confirmed roster of barangay officials. Please check the barangay office or the Announcements section in BrgyLink for the list of current officials.",
497
+ "tagalog": "Ang chatbot ay walang kumpirmadong roster ng mga opisyal ng barangay. Tingnan po ang opisina ng barangay o ang Announcements sa BrgyLink para sa listahan ng kasalukuyang mga opisyal.",
498
+ "ilocano": "Ti chatbot ket awan ti nakumpirma a roster dagiti opisyal ti barangay. Kitaen ti opisina ti barangay wenno ti Announcements iti BrgyLink para iti listaan dagiti agdama nga opisyal.",
499
+ "pangasinan": "Say chatbot et anggapoy nakumpirman roster na saray opisyal na barangay. Nengnengen so opisina na barangay odino so Announcements ed BrgyLink para ed listaan na saray opisyal natan."
500
+ },
501
+ "verified": false,
502
+ "verified_by": null,
503
+ "verified_at": null,
504
+ "expires_at": null,
505
+ "source": null,
506
+ "last_reviewed_at": null,
507
+ "review_notes": "",
508
+ "content_type": "barangay_fact",
509
+ "requires_verification": true
510
+ },
511
+ {
512
+ "intent": "greeting",
513
+ "title": "Greeting",
514
+ "needs_staff_verification": false,
515
+ "requirements": [],
516
+ "fee_php": null,
517
+ "processing_time": null,
518
+ "answer": {
519
+ "english": "Hello! I am BrgyLink AI, the virtual assistant for Barangay Bagong Pag-asa, San Jacinto. How can I help you with document requests, blotter reports, announcements, or barangay services today?",
520
+ "tagalog": "Magandang araw! Ako ang BrgyLink AI, ang virtual assistant ng Barangay Bagong Pag-asa, San Jacinto. Paano kita matutulungan sa mga dokumento, blotter, anunsyo, o serbisyo ng barangay ngayon?",
521
+ "ilocano": "Naimbag nga aldaw! Siak ni BrgyLink AI, ti virtual assistant ti Barangay Bagong Pag-asa, San Jacinto. Kasano a makatulongak kadagiti dokumento, blotter, anunsio, wenno serbisyo ti barangay tatta?",
522
+ "pangasinan": "Maabig ya agew! Siak si BrgyLink AI, so virtual assistant na Barangay Bagong Pag-asa, San Jacinto. Panon ta kan natulongan ed saray dokumento, blotter, anunsyo, odino serbisyo na barangay natan?"
523
+ },
524
+ "verified": false,
525
+ "verified_by": null,
526
+ "verified_at": null,
527
+ "expires_at": null,
528
+ "source": null,
529
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
530
+ "review_notes": "Imported non-service intent",
531
+ "content_type": "system_copy",
532
+ "requires_verification": false
533
+ },
534
+ {
535
+ "intent": "goodbye",
536
+ "title": "Goodbye",
537
+ "needs_staff_verification": false,
538
+ "requirements": [],
539
+ "fee_php": null,
540
+ "processing_time": null,
541
+ "answer": {
542
+ "english": "Thank you for using BrgyLink AI! If you need anything else, I am always here to assist. Stay safe and have a wonderful day!",
543
+ "tagalog": "Maraming salamat sa paggamit ng BrgyLink AI! Kung may iba ka pang katanungan, narito lamang ako handang tumulong. Mag-ingat ka palagi at magandang araw!",
544
+ "ilocano": "Agyamanak unay iti panagusar iti BrgyLink AI! No adda pay masapulmo, situtulokak a tumulong. Agannadka kanayon ken naimbag nga aldaw!",
545
+ "pangasinan": "Balbaleg ya salamat ed panangusar mo ed BrgyLink AI! No wala ni ray tepet mo diad ontumbok, wadya ak labat ya ontulong. Man-ingat ka lanang tan maabig ya agew!"
546
+ },
547
+ "verified": false,
548
+ "verified_by": null,
549
+ "verified_at": null,
550
+ "expires_at": null,
551
+ "source": null,
552
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
553
+ "review_notes": "Imported non-service intent",
554
+ "content_type": "system_copy",
555
+ "requires_verification": false
556
+ },
557
+ {
558
+ "intent": "thanks",
559
+ "title": "Thanks",
560
+ "needs_staff_verification": false,
561
+ "requirements": [],
562
+ "fee_php": null,
563
+ "processing_time": null,
564
+ "answer": {
565
+ "english": "You're very welcome! Glad to assist you as a resident of Barangay Bagong Pag-asa. Feel free to ask if you need further guidance.",
566
+ "tagalog": "Walang anuman! Ikinagagalak kong makatulong sa iyo bilang residente ng Barangay Bagong Pag-asa. Huwag mag-atubiling magtanong kung may kailangan ka pa.",
567
+ "ilocano": "Awan aniamanna kabsat! Naragsakak a makatulong kenka kas kailian iti Barangay Bagong Pag-asa. Agsaludsodka laeng no adda pay masapulmo.",
568
+ "pangasinan": "Anggapoy kabaliksan kabaleyan! Maliket ak ya makatulong ed sika bilang residente na Barangay Bagong Pag-asa. Tepet ka labat no wala ni nakaukol mo."
569
+ },
570
+ "verified": false,
571
+ "verified_by": null,
572
+ "verified_at": null,
573
+ "expires_at": null,
574
+ "source": null,
575
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
576
+ "review_notes": "Imported non-service intent",
577
+ "content_type": "system_copy",
578
+ "requires_verification": false
579
+ },
580
+ {
581
+ "intent": "solo_parent",
582
+ "title": "Solo Parent",
583
+ "needs_staff_verification": false,
584
+ "requirements": [],
585
+ "fee_php": null,
586
+ "processing_time": null,
587
+ "answer": {
588
+ "english": "For information about Solo Parent ID and welfare benefits in Barangay Bagong Pag-asa, please visit the barangay office or check the Announcements section in BrgyLink.",
589
+ "tagalog": "Para sa impormasyon tungkol sa Solo Parent ID at mga benepisyo sa Barangay Bagong Pag-asa, bumisita sa opisina ng barangay o tingnan ang Announcements sa BrgyLink.",
590
+ "ilocano": "Para iti impormasion maipapan iti Solo Parent ID ken dagiti benepisio iti Barangay Bagong Pag-asa, bisitaen ti opisina ti barangay wenno kitaen ti Announcements iti BrgyLink.",
591
+ "pangasinan": "Para ed impormasyon nipaakar ed Solo Parent ID tan saray benepisyo ed Barangay Bagong Pag-asa, bisitaen so opisina na barangay odino nengnengen so Announcements ed BrgyLink."
592
+ },
593
+ "verified": false,
594
+ "verified_by": null,
595
+ "verified_at": null,
596
+ "expires_at": null,
597
+ "source": null,
598
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
599
+ "review_notes": "Imported non-service intent",
600
+ "content_type": "barangay_fact",
601
+ "requires_verification": true
602
+ },
603
+ {
604
+ "intent": "senior_citizen",
605
+ "title": "Senior Citizen",
606
+ "needs_staff_verification": false,
607
+ "requirements": [],
608
+ "fee_php": null,
609
+ "processing_time": null,
610
+ "answer": {
611
+ "english": "For information about Senior Citizen ID and OSCA services in Barangay Bagong Pag-asa, please visit the barangay office or check the Announcements section in BrgyLink.",
612
+ "tagalog": "Para sa impormasyon tungkol sa Senior Citizen ID at mga serbisyo ng OSCA sa Barangay Bagong Pag-asa, bumisita sa opisina ng barangay o tingnan ang Announcements sa BrgyLink.",
613
+ "ilocano": "Para iti impormasion maipapan iti Senior Citizen ID ken serbisio ti OSCA iti Barangay Bagong Pag-asa, bisitaen ti opisina ti barangay wenno kitaen ti Announcements iti BrgyLink.",
614
+ "pangasinan": "Para ed impormasyon nipaakar ed Senior Citizen ID tan saray serbisyo na OSCA ed Barangay Bagong Pag-asa, bisitaen so opisina na barangay odino nengnengen so Announcements ed BrgyLink."
615
+ },
616
+ "verified": false,
617
+ "verified_by": null,
618
+ "verified_at": null,
619
+ "expires_at": null,
620
+ "source": null,
621
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
622
+ "review_notes": "Imported non-service intent",
623
+ "content_type": "barangay_fact",
624
+ "requires_verification": true
625
+ },
626
+ {
627
+ "intent": "lupon",
628
+ "title": "Lupon",
629
+ "needs_staff_verification": false,
630
+ "requirements": [],
631
+ "fee_php": null,
632
+ "processing_time": null,
633
+ "answer": {
634
+ "english": "For disputes that may be resolved at the barangay level, the Lupon Tagapamayapa handles conciliation and mediation. Please visit the barangay office to file a complaint or inquire about the process.",
635
+ "tagalog": "Para sa mga alitang maaaring resolbahin sa antas ng barangay, ang Lupon Tagapamayapa ang nag-aasikaso ng conciliation at mediation. Bumisita sa opisina ng barangay para mag-file ng reklamo o magtanong tungkol sa proseso.",
636
+ "ilocano": "Para kadagiti kolkol a mabalin a marisut iti lebel ti barangay, ti Lupon Tagapamayapa ti mangasikaso iti conciliation ken mediation. Bisitaen ti opisina ti barangay tapno ag-file iti reklamo wenno saludsoden ti proseso.",
637
+ "pangasinan": "Para ed saray alitan ya nayarin resolbien ed lebel na barangay, say Lupon Tagapamayapa so mangaasikaso na conciliation tan mediation. Bisitaen so opisina na barangay pian man-file na reklamo odino mantepet nipaakar ed proseso."
638
+ },
639
+ "verified": false,
640
+ "verified_by": null,
641
+ "verified_at": null,
642
+ "expires_at": null,
643
+ "source": null,
644
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
645
+ "review_notes": "Imported non-service intent",
646
+ "content_type": "barangay_fact",
647
+ "requires_verification": true
648
+ },
649
+ {
650
+ "intent": "aics_assistance",
651
+ "title": "Aics Assistance",
652
+ "needs_staff_verification": false,
653
+ "requirements": [],
654
+ "fee_php": null,
655
+ "processing_time": null,
656
+ "answer": {
657
+ "english": "For financial assistance, medical or burial aid (AICS), relief, or social welfare programs, please visit the Barangay Bagong Pag-asa office to inquire about available services and requirements.",
658
+ "tagalog": "Para sa tulong pinansyal, medikal o burial aid (AICS), relief, o mga programa ng social welfare, bumisita po sa opisina ng Barangay Bagong Pag-asa para magtanong tungkol sa mga available na serbisyo at kinakailangan.",
659
+ "ilocano": "Para iti tulong-pinansyal, medikal wenno burial aid (AICS), relief, wenno programa ti social welfare, bisitaen ti opisina ti Barangay Bagong Pag-asa tapno saludsoden dagiti magun-od a serbisio ken kasapulan.",
660
+ "pangasinan": "Para ed tulong-pinansyal, medikal odino burial aid (AICS), relief, odino saray programa na social welfare, bisitaen so opisina na Barangay Bagong Pag-asa pian mantepet nipaakar ed saray serbisyo tan kailangan."
661
+ },
662
+ "verified": false,
663
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
664
+ "review_notes": "Imported non-service intent",
665
+ "verified_by": null,
666
+ "verified_at": null,
667
+ "expires_at": null,
668
+ "source": null,
669
+ "content_type": "barangay_fact",
670
+ "requires_verification": true
671
+ },
672
+ {
673
+ "intent": "document_request",
674
+ "title": "Document Request",
675
+ "needs_staff_verification": false,
676
+ "requirements": [],
677
+ "fee_php": null,
678
+ "processing_time": null,
679
+ "answer": {
680
+ "english": "Which document do you need? You can request a Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, or Barangay ID through Document Requests in BrgyLink.",
681
+ "tagalog": "Anong dokumento po ang kailangan ninyo? Maaari kayong humiling ng Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, o Barangay ID sa pamamagitan ng Document Requests sa BrgyLink.",
682
+ "ilocano": "Ania a dokumento ti kasapulam? Mabalin ti agkiddaw iti Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, wenno Barangay ID babaen ti Document Requests iti BrgyLink.",
683
+ "pangasinan": "Antoy dokumento ya kailangan mo? Nayari kayon mangala na Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, odino Barangay ID diad Document Requests ed BrgyLink."
684
+ },
685
+ "verified": false,
686
+ "verified_by": null,
687
+ "verified_at": null,
688
+ "expires_at": null,
689
+ "source": null,
690
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
691
+ "review_notes": "Imported non-service intent",
692
+ "content_type": "barangay_fact",
693
+ "requires_verification": true
694
+ },
695
+ {
696
+ "intent": "language_support",
697
+ "title": "Language Support",
698
+ "needs_staff_verification": false,
699
+ "requirements": [],
700
+ "fee_php": null,
701
+ "processing_time": null,
702
+ "answer": {
703
+ "english": "Yes! I can understand and respond in Pangasinan, Ilocano, and Tagalog (as well as English). Feel free to ask your question in any of these languages!",
704
+ "tagalog": "Opo! Nakakaintindi at nakakasagot ako sa tatlong pangunahing wika: Tagalog, Ilocano, at Pangasinan (at pati na rin sa English). Maaari kang magtanong sa alinman sa mga wikang ito anumang oras!",
705
+ "ilocano": "Wen! Makaawat ken makasungbatak iti tallo a kangrunaan a pagsasao: Ilocano, Tagalog, ken Pangasinan (ken kasta met iti English). Mabalinmo ti agsaludsod iti aniaman kadagitoy a wika!",
706
+ "pangasinan": "On! Makatalos tan makasungbat ak ed taloran manunan salita: Pangasinan, Tagalog, tan Ilocano (tan pati met ed English). Nayari kayon mantepet ed dinanman ed sarayan salita ed anggan anton oras!"
707
+ },
708
+ "verified": false,
709
+ "verified_by": null,
710
+ "verified_at": null,
711
+ "expires_at": null,
712
+ "source": null,
713
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
714
+ "review_notes": "Imported non-service intent",
715
+ "content_type": "system_copy",
716
+ "requires_verification": false
717
+ },
718
+ {
719
+ "intent": "about_app",
720
+ "title": "About App",
721
+ "needs_staff_verification": false,
722
+ "requirements": [],
723
+ "fee_php": null,
724
+ "processing_time": null,
725
+ "answer": {
726
+ "english": "BrgyLink AI can guide you through BrgyLink topics: registration and email verification, password recovery, document requests, blotter reports, feedback, announcements, civic tasks, events, and where to check officials information. You may ask in English, Tagalog, Ilocano, or Pangasinan. I cannot access accounts, passwords, OTPs, IDs, personal submissions, live schedules, or emergency services.",
727
+ "tagalog": "Makakatulong ang BrgyLink AI sa mga paksa sa BrgyLink: registration at email verification, password recovery, document requests, blotter reports, feedback, announcements, civic tasks, events, at kung saan titingnan ang impormasyon tungkol sa officials. Maaari kang magtanong sa English, Tagalog, Ilocano, o Pangasinan. Hindi ko naa-access ang accounts, passwords, OTPs, IDs, personal submissions, live schedules, o emergency services.",
728
+ "ilocano": "Makatulong ti BrgyLink AI kadagiti paksa iti BrgyLink: registration ken email verification, password recovery, document requests, blotter reports, feedback, announcements, civic tasks, events, ken sadino a kitaen ti impormasyon maipapan kadagiti officials. Mabalin a mangsaludsod iti English, Tagalog, Ilocano, wenno Pangasinan. Saan a ma-access ti chatbot dagiti account, password, OTP, ID, personal submission, live schedule, wenno emergency services.",
729
+ "pangasinan": "Nayarin makatulong so BrgyLink AI ed saray paksa ed BrgyLink: registration tan email verification, password recovery, document requests, blotter reports, feedback, announcements, civic tasks, events, tan iner ya nengnengen so impormasyon nipaakar ed officials. Nayarin magtanong ed English, Tagalog, Ilocano, odino Pangasinan. Ag na-access na chatbot so accounts, passwords, OTPs, IDs, personal submissions, live schedules, odino emergency services."
730
+ },
731
+ "verified": false,
732
+ "verified_by": null,
733
+ "verified_at": null,
734
+ "expires_at": null,
735
+ "source": null,
736
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
737
+ "review_notes": "Imported non-service intent",
738
+ "content_type": "system_copy",
739
+ "requires_verification": false
740
+ },
741
+ {
742
+ "intent": "voter_registration",
743
+ "title": "Voter Registration",
744
+ "needs_staff_verification": false,
745
+ "requirements": [],
746
+ "fee_php": null,
747
+ "processing_time": null,
748
+ "answer": {
749
+ "english": "For voter registration information, please visit the barangay office or COMELEC. Check the Announcements section in BrgyLink for any posted registration schedules.",
750
+ "tagalog": "Para sa impormasyon tungkol sa voter registration, bumisita sa opisina ng barangay o COMELEC. Tingnan ang Announcements sa BrgyLink para sa mga naka-post na schedule ng registration.",
751
+ "ilocano": "Para iti impormasion maipapan iti voter registration, bisitaen ti opisina ti barangay wenno COMELEC. Kitaen ti Announcements iti BrgyLink para kadagiti naipablaak nga iskedyul ti registration.",
752
+ "pangasinan": "Para ed impormasyon nipaakar ed voter registration, bisitaen so opisina na barangay odino COMELEC. Nengnengen so Announcements ed BrgyLink para ed saray aka-post ya schedule na registration."
753
+ },
754
+ "verified": false,
755
+ "verified_by": null,
756
+ "verified_at": null,
757
+ "expires_at": null,
758
+ "source": null,
759
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
760
+ "review_notes": "Imported non-service intent",
761
+ "content_type": "barangay_fact",
762
+ "requires_verification": true
763
+ },
764
+ {
765
+ "intent": "health_services",
766
+ "title": "Health Services",
767
+ "needs_staff_verification": false,
768
+ "requirements": [],
769
+ "fee_php": null,
770
+ "processing_time": null,
771
+ "answer": {
772
+ "english": "For information about health services at the Barangay Health Center, including available programs and schedules, please visit the health center or check the Announcements section in BrgyLink.",
773
+ "tagalog": "Para sa impormasyon tungkol sa mga serbisyong pangkalusugan sa Barangay Health Center, kasama ang mga programa at schedule, bumisita sa health center o tingnan ang Announcements sa BrgyLink.",
774
+ "ilocano": "Para iti impormasion maipapan kadagiti serbisio ti salun-at iti Barangay Health Center, agraman dagiti programa ken iskediul, bisitaen ti health center wenno kitaen ti Announcements iti BrgyLink.",
775
+ "pangasinan": "Para ed impormasyon nipaakar ed saray serbisyo na salud ed Barangay Health Center, kaibay saray programa tan schedule, bisitaen so health center odino nengnengen so Announcements ed BrgyLink."
776
+ },
777
+ "verified": false,
778
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
779
+ "review_notes": "Imported non-service intent",
780
+ "verified_by": null,
781
+ "verified_at": null,
782
+ "expires_at": null,
783
+ "source": null,
784
+ "content_type": "barangay_fact",
785
+ "requires_verification": true
786
+ },
787
+ {
788
+ "intent": "events",
789
+ "title": "Community Events",
790
+ "needs_staff_verification": false,
791
+ "requirements": [],
792
+ "fee_php": null,
793
+ "processing_time": null,
794
+ "answer": {
795
+ "english": "Open the Events tab under Civic Tasks & Community Events, choose an active event, and read its date, time, location, and instructions. If proof is requested, take a clear participation photo and tap Submit Participation Proof. Submitted is not the same as Approved. Check the app's displayed status or ask the barangay office if no review status is shown. The chatbot cannot see live event details or your personal approval result.",
796
+ "tagalog": "Buksan ang Events tab sa Civic Tasks & Community Events, pumili ng aktibong event, at basahin ang petsa, oras, lugar, at tagubilin. Kung kailangan ng proof, kumuha ng malinaw na larawan ng pakikilahok at i-tap ang Submit Participation Proof. Hindi pareho ang Submitted at Approved. Tingnan ang status na ipinapakita ng app o magtanong sa opisina ng barangay kung walang review status. Hindi nakikita ng chatbot ang live event details o personal na approval result.",
797
+ "ilocano": "Lukatan ti Events tab iti Civic Tasks & Community Events, piliem ti aktibo nga event, ken basaem ti petsa, oras, lugar, ken tagubilin. No kasapulan ti proof, alaem ti nalawag a ladawan ti panagpaset ken i-tap ti Submit Participation Proof. Saan nga agpada ti Submitted ken Approved. Kitaem ti status iti app wenno agsaludsod iti opisina ti barangay no awan ti review status. Saan a makita ti chatbot ti live event details wenno personal a approval result.",
798
+ "pangasinan": "Lukatan so Events tab ed Civic Tasks & Community Events, piliyen so aktibon event, tan basaen so petsa, oras, lugar, tan tagubilin. No kasapulan so proof, alaen so malinew ya litrato na pakikiba tan i-tap so Submit Participation Proof. Agpareho so Submitted tan Approved. Nengnengen so status ed app odino mantepet ed opisina na barangay no anggapo so review status. Ag nanengneng na chatbot so live event details odino personal ya approval result."
799
+ },
800
+ "verified": false,
801
+ "verified_by": null,
802
+ "verified_at": null,
803
+ "expires_at": null,
804
+ "source": null,
805
+ "last_reviewed_at": null,
806
+ "review_notes": "App navigation copy only; event details remain in the live Events screen.",
807
+ "content_type": "system_copy",
808
+ "requires_verification": false
809
+ },
810
+ {
811
+ "intent": "fallback",
812
+ "title": "Fallback",
813
+ "needs_staff_verification": false,
814
+ "requirements": [],
815
+ "fee_php": null,
816
+ "processing_time": null,
817
+ "answer": {
818
+ "english": "I specialize in Barangay Bagong Pag-asa services including Document Requests (Clearance, Indigency, Residency, ID), Blotter Reports, AICS Assistance, Lupon mediation, and Office details. Could you please rephrase or ask about barangay services?",
819
+ "tagalog": "Ipagpaumanhin mo, hindi ko lubos na naintindihan ang iyong tanong. Makakatulong ako sa mga serbisyo ng Barangay Bagong Pag-asa tulad ng Document Requests (Clearance, Indigency, Residency, ID), Blotter Reports, Ayuda/AICS, Lupon, at Opisina. Maaari mo bang linawin o itanong ang tungkol sa serbisyo ng barangay?",
820
+ "ilocano": "Dispensaren, saanko unay a naawatan ti saludsodmo. Makatulongak kadagiti serbisyo ti Barangay Bagong Pag-asa kas iti Document Requests (Clearance, Indigency, Residency, ID), Blotter Reports, Ayuda/AICS, Lupon, ken Opisina. Mabalinmo kadi a palawagen wenno agsaludsod maipapan kadagiti serbisyo ti barangay?",
821
+ "pangasinan": "Pasensya la kabaleyan, agko unay atalosan so tepet mo. Makatulong ak ed saray serbisyo na Barangay Bagong Pag-asa singa Document Requests (Clearance, Indigency, Residency, ID), Blotter Reports, Ayuda/AICS, Lupon, tan Opisina. Nayarim kasi ya ipalinew odino mantepet nipaakar ed serbisyo na barangay?"
822
+ },
823
+ "verified": false,
824
+ "verified_by": null,
825
+ "verified_at": null,
826
+ "expires_at": null,
827
+ "source": null,
828
+ "last_reviewed_at": "2026-09-29T00:00:00Z",
829
+ "review_notes": "Imported non-service intent",
830
+ "content_type": "system_copy",
831
+ "requires_verification": false
832
+ },
833
+ {
834
+ "intent": "out_of_scope",
835
+ "title": "Out Of Scope",
836
+ "answer": {
837
+ "english": "I'm a barangay assistant. I don't know about that.",
838
+ "tagalog": "Ako ay isang barangay assistant. Hindi ko alam ang tungkol diyan.",
839
+ "ilocano": "Siak ket barangay assistant. Saanko nga ammo dayta.",
840
+ "pangasinan": "Siak et barangay assistant. Agko amta itan."
841
+ },
842
+ "verified": false,
843
+ "verified_by": null,
844
+ "verified_at": null,
845
+ "expires_at": null,
846
+ "source": null,
847
+ "last_reviewed_at": null,
848
+ "review_notes": "",
849
+ "content_type": "system_copy",
850
+ "requires_verification": false
851
+ },
852
+ {
853
+ "intent": "safety_threat",
854
+ "title": "Safety Threat",
855
+ "answer": {
856
+ "english": "Please contact emergency services (911) immediately.",
857
+ "tagalog": "Mangyaring tumawag sa emergency services (911) kaagad.",
858
+ "ilocano": "Pangngaasi nga umawag ti emergency services (911) a dagus.",
859
+ "pangasinan": "Tawagan so emergency services (911) a tampol."
860
+ },
861
+ "verified": false,
862
+ "verified_by": null,
863
+ "verified_at": null,
864
+ "expires_at": null,
865
+ "source": null,
866
+ "last_reviewed_at": null,
867
+ "review_notes": "",
868
+ "content_type": "barangay_fact",
869
+ "requires_verification": true
870
+ },
871
+ {
872
+ "intent": "medical_non_emergency",
873
+ "title": "Medical Non Emergency",
874
+ "answer": {
875
+ "english": "Please visit the barangay health center during operating hours.",
876
+ "tagalog": "Mangyaring pumunta sa health center ng barangay habang bukas.",
877
+ "ilocano": "Pangngaasi nga mapan iti health center ti barangay kabayatan nga lukat.",
878
+ "pangasinan": "Umapay ed health center na barangay legan na pakanengnengan."
879
+ },
880
+ "verified": false,
881
+ "verified_by": null,
882
+ "verified_at": null,
883
+ "expires_at": null,
884
+ "source": null,
885
+ "last_reviewed_at": null,
886
+ "review_notes": "",
887
+ "content_type": "barangay_fact",
888
+ "requires_verification": true
889
+ }
890
+ ]
891
+ }
chatbot_runtime/service.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP-independent request handling for the bundled BrgyLink chatbot."""
2
+
3
+ import re
4
+ import threading
5
+ import time
6
+ from collections import OrderedDict
7
+
8
+ from .smart_classifier import (
9
+ MODEL_VERSION,
10
+ handle_message,
11
+ load_model,
12
+ new_session,
13
+ )
14
+
15
+
16
+ LANG_ALIASES = {
17
+ "en": "english",
18
+ "english": "english",
19
+ "fil": "tagalog",
20
+ "tl": "tagalog",
21
+ "tagalog": "tagalog",
22
+ "ilo": "ilocano",
23
+ "ilocano": "ilocano",
24
+ "pag": "pangasinan",
25
+ "pangasinan": "pangasinan",
26
+ }
27
+ LANG_DISPLAY = {
28
+ "pangasinan": "Pangasinan",
29
+ "ilocano": "Ilocano",
30
+ "tagalog": "Tagalog / Filipino",
31
+ "english": "English",
32
+ }
33
+ SESSION_ID_PATTERN = re.compile(r"[A-Za-z0-9_.-]{1,128}")
34
+
35
+
36
+ class ChatService:
37
+ """Bounded in-memory chat state around the safety-constrained classifier."""
38
+
39
+ max_message_length = 2000
40
+ max_sessions = 1000
41
+ session_ttl_seconds = 3600
42
+
43
+ def __init__(self):
44
+ self.model = load_model()
45
+ self.sessions = OrderedDict()
46
+ self.session_last_seen = {}
47
+ self.lock = threading.Lock()
48
+
49
+ def clear_sessions(self):
50
+ with self.lock:
51
+ self.sessions.clear()
52
+ self.session_last_seen.clear()
53
+
54
+ def _prune_sessions(self, now):
55
+ expired = [
56
+ key for key, seen in self.session_last_seen.items()
57
+ if now - seen >= self.session_ttl_seconds
58
+ ]
59
+ for key in expired:
60
+ self.sessions.pop(key, None)
61
+ self.session_last_seen.pop(key, None)
62
+ while len(self.sessions) >= self.max_sessions:
63
+ key, _session = self.sessions.popitem(last=False)
64
+ self.session_last_seen.pop(key, None)
65
+
66
+ def respond(self, payload):
67
+ """Return a stable (status_code, JSON-safe payload) response tuple."""
68
+ if not isinstance(payload, dict):
69
+ return 400, {"error": "A JSON object is required."}
70
+
71
+ message = payload.get("message")
72
+ if not isinstance(message, str) or not message.strip():
73
+ return 400, {"error": "Please enter a question or message."}
74
+ message = message.strip()
75
+ if len(message) > self.max_message_length:
76
+ return 400, {"error": f"Messages must be at most {self.max_message_length} characters."}
77
+
78
+ session_id = payload.get("session_id")
79
+ if session_id is not None and (
80
+ not isinstance(session_id, str) or not SESSION_ID_PATTERN.fullmatch(session_id)
81
+ ):
82
+ return 400, {
83
+ "error": "Use a non-personal session_id of 1 to 128 letters, digits, dots, underscores or hyphens."
84
+ }
85
+
86
+ language = payload.get("language")
87
+ if language is not None:
88
+ if not isinstance(language, str) or language.lower().strip() not in LANG_ALIASES:
89
+ return 400, {"error": "Unsupported language."}
90
+ language = LANG_ALIASES[language.lower().strip()]
91
+
92
+ try:
93
+ started_at = time.time()
94
+ with self.lock:
95
+ now = time.monotonic()
96
+ if session_id and session_id not in self.sessions:
97
+ self._prune_sessions(now)
98
+ self.sessions[session_id] = new_session()
99
+ elif session_id and now - self.session_last_seen.get(session_id, 0) >= self.session_ttl_seconds:
100
+ self.sessions[session_id] = new_session()
101
+
102
+ chat_session = self.sessions[session_id] if session_id else new_session()
103
+ if session_id:
104
+ self.sessions.move_to_end(session_id)
105
+ self.session_last_seen[session_id] = now
106
+ if language:
107
+ chat_session["preferred_language"] = language
108
+ result = handle_message(message, session=chat_session, model=self.model)
109
+
110
+ detected_language = result.get("language", "english")
111
+ return 200, {
112
+ "response": result["response"],
113
+ "intent": result.get("intent", "unknown"),
114
+ "confidence": result.get("similarity", 1.0),
115
+ "language": detected_language,
116
+ "language_name": LANG_DISPLAY.get(detected_language, detected_language.capitalize()),
117
+ "processing_time_ms": round((time.time() - started_at) * 1000, 2),
118
+ "model_version": MODEL_VERSION,
119
+ }
120
+ except Exception:
121
+ return 500, {"error": "Failed to process chat message. Please try again."}
chatbot_runtime/smart_classifier.py ADDED
@@ -0,0 +1,909 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BrgyLink Smart Classifier v7 — Prototype Multilingual Offline NLP Engine.
3
+ Barangay assistant prototype for Barangay Bagong Pag-asa, San Jacinto.
4
+ Languages supported: Pangasinan, Ilocano, Tagalog, English.
5
+
6
+ IMPORTANT: This is a prototype. Answers come from knowledge_base.json.
7
+ Unverified facts are flagged. The chatbot does not provide legal or medical
8
+ advice and cannot dispatch emergency services.
9
+ """
10
+
11
+ import json
12
+ import hashlib
13
+ import math
14
+ import os
15
+ import pickle
16
+ import re
17
+ import tempfile
18
+ from collections import Counter, defaultdict
19
+
20
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
21
+ INTENTS_FILE = os.path.join(BASE_DIR, "data", "intents_brgylink_curated.json")
22
+ KB_FILE = os.path.join(BASE_DIR, "knowledge_base.json")
23
+ MODEL_FILE = os.path.join(BASE_DIR, "smart_classifier.pkl")
24
+ MODEL_VERSION = 7
25
+ FALLBACK = "out_of_scope"
26
+
27
+ # --- Tuned thresholds ---
28
+ CONFIDENCE_THRESHOLD = 0.18 # minimum score to accept an intent
29
+ MARGIN_THRESHOLD = 0.06 # minimum gap between #1 and #2 intents
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Document aliases -> standard display name
33
+ # ---------------------------------------------------------------------------
34
+ DOCUMENTS = {
35
+ "barangay clearance": "Barangay Clearance",
36
+ "clearance": "Barangay Clearance",
37
+ "indigency": "Certificate of Indigency",
38
+ "certificate of indigency": "Certificate of Indigency",
39
+ "residency": "Certificate of Residency",
40
+ "certificate of residency": "Certificate of Residency",
41
+ "business clearance": "Business Clearance",
42
+ "business permit": "Business Clearance",
43
+ "good moral": "Certificate of Good Moral Character",
44
+ "good moral character": "Certificate of Good Moral Character",
45
+ "barangay id": "Barangay ID",
46
+ "bgy id": "Barangay ID",
47
+ "barangay i.d": "Barangay ID",
48
+ "barangay identification": "Barangay ID",
49
+ }
50
+
51
+ DOC_TO_INTENT = {
52
+ "Barangay Clearance": "clearance",
53
+ "Certificate of Indigency": "indigency",
54
+ "Certificate of Residency": "residency",
55
+ "Business Clearance": "business_permit",
56
+ "Certificate of Good Moral Character": "good_moral",
57
+ "Barangay ID": "barangay_id",
58
+ }
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Alias normalization
62
+ # ---------------------------------------------------------------------------
63
+ ALIASES: list[tuple[re.Pattern, str]] = [
64
+ (re.compile(r"\bbgy\b"), "barangay"),
65
+ (re.compile(r"\bbrgy\b"), "barangay"),
66
+ (re.compile(r"\bcert\b"), "certificate"),
67
+ (re.compile(r"\bclrnce\b"), "clearance"),
68
+ (re.compile(r"\bindgncy\b"), "indigency"),
69
+ (re.compile(r"\bindigensy\b"), "indigency"),
70
+ (re.compile(r"\bindigincy\b"), "indigency"),
71
+ (re.compile(r"\bresdncy\b"), "residency"),
72
+ (re.compile(r"\bbus\.?\s*pmt\b"), "business permit"),
73
+ (re.compile(r"\bgood\s*moral\b"), "good moral"),
74
+ (re.compile(r"\bblotter\b"), "blotter report"),
75
+ (re.compile(r"\baics\b"), "aics assistance"),
76
+ (re.compile(r"\bdswd\b"), "dswd assistance"),
77
+ (re.compile(r"\bosca\b"), "osca senior citizen"),
78
+ (re.compile(r"\bbhc\b"), "barangay health center"),
79
+ (re.compile(r"\b4ps\b"), "4ps pantawid pamilya"),
80
+ (re.compile(r"\bsap\b"), "social amelioration program"),
81
+ (re.compile(r"\bkp\b"), "katarungang pambarangay"),
82
+ (re.compile(r"\bcomelec\b"), "comelec voter registration"),
83
+ (re.compile(r"\botp\b"), "one time password otp verification"),
84
+ (re.compile(r"\bpwd\b"), "person with disability pwd"),
85
+ ]
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # SAFETY-CRITICAL: Pre-classifier keyword rules
89
+ # These ALWAYS override the TF-IDF classifier.
90
+ # ---------------------------------------------------------------------------
91
+ _EMERGENCY_RE = re.compile(
92
+ r"\b(sunog|apoy|uram|fire|911|ambulance|ambulansya|saklolo|tulong\s+tulong|aksidente|"
93
+ r"inatake|heart\s*attack|aktibong\s*krimen|hostage|crime\s*in\s*progress|"
94
+ r"nahulog|drowning|earthquake|lindol|bagyo|typhoon|flood|baha|"
95
+ r"nawawala\w*\s*(na\s*)?(bata|tao|anak)|kidnap|active\s*shooter|nawawalang|"
96
+ r"weapons?|active\s*assault|imminent\s*killing|severe\s*bleeding|inability\s*to\s*breathe|"
97
+ r"unconsciousness|poisoning|binubugbog|binugbog|sinasaksak|may\s*baril|binabaril|"
98
+ r"assault|sinakal|pinugutan|patay|pinatay|pumatay|self[\s\-]?harm|suicide|"
99
+ r"papatayin|patayin|gustong\s*mamatay|ayaw\s*(ko\s*)?na\s*mabuhay|kill|nananaksak|"
100
+ r"nagdudugo|bleeding|hirap\s*huminga|difficulty\s*breathing|"
101
+ r"(?:cannot|can\s*t|cant|unable\s*to)\s*breathe|"
102
+ r"(?:not|stopped|stops)\s*breathing|chok(?:e|es|ed|ing)|"
103
+ r"(?:cannot|can\s*t|cant)\s*catch\s*(?:my|their|his|her|our)?\s*breath|"
104
+ r"struggling\s*to\s*breathe|gasping\s*(?:for\s*)?air|hurt\s*myself|"
105
+ r"hindi\s*(?:na\s*)?makahinga|di\s*makahinga|"
106
+ r"nauubusan\s*(?:ng\s*)?hangin|saan\s*a\s*makaanges|ag\s*makainawa|"
107
+ r"end\s*my\s*life|wakasan\s*(?:ang\s*)?(?:aking\s*)?buhay)\b",
108
+ re.IGNORECASE,
109
+ )
110
+
111
+ _THREAT_RE = re.compile(
112
+ r"\b(nangbabanta|nagbanta|banta|sinasaktan|inaabuso|"
113
+ r"domestic\s*violence|violence|abuse\w*|sinaktan|pinagsasaktan|"
114
+ r"hinoldap|holdup|rape|pangmomolestiya|molestiya|"
115
+ r"stalker|stalking|nananakit|pinagbabantaan|binabanta|"
116
+ r"threat\w*|harass\w*|haras\w*|pang-?aabuso|"
117
+ r"nag-aaway|nag-aalburoto|nagwawala|naghaharas|"
118
+ r"nagbabanta|nang-?aaway|mangdadakip)\b",
119
+ re.IGNORECASE,
120
+ )
121
+
122
+ _MEDICAL_SYMPTOM_RE = re.compile(
123
+ r"\b(lagnat|fever|ubo|cough|sakit\s*(ng|na)\s*(ulo|tiyan|dibdib|katawan)|"
124
+ r"pain|masakit|sumasakit|sugat|wound|allergy|"
125
+ r"hilo|dizzy|diarrhea|vomit|nausea|nilalagnat|may\s*sakit|"
126
+ r"nahihilo|nasusuka|"
127
+ r"sore|infection|impeksyon|skin\s*rash|singaw|"
128
+ r"buntis|pregnant|prenatal|masakit\s*ang|anak\s*ko\s*may\s*sakit|"
129
+ r"my\s*child\s*(?:has\s*a\s*fever|is\s*sick|needs\s*help))\b",
130
+ re.IGNORECASE,
131
+ )
132
+
133
+ _LEGAL_RE = re.compile(
134
+ r"\b(abogado|lawyer|attorney|legal\s*advice|kaso|court\s*case|"
135
+ r"legal\s*consultation|fiscal|hukom|judge|demanda|magdemanda|"
136
+ r"file\s*(?:a\s*)?case|sue|kasuhan|sampahan)\b",
137
+ re.IGNORECASE,
138
+ )
139
+
140
+ _OUT_OF_SCOPE_RE = re.compile(
141
+ r"\b(passport|visa|nbi\s*clearance|psa|birth\s*certificate|"
142
+ r"driver.?s?\s*license|sss|pag.?ibig|phil\s*health|"
143
+ r"school\s*enrollment|tuition|"
144
+ r"basketball|nba|movie|joke|sing\s*(me\s*)?a\s*song|poem|"
145
+ r"weather\s*in|president\s*of|capital\s*of|recipe|"
146
+ r"pizza|burger|game|football|calculus|cryptocurrency|bitcoin|stock\s*market|"
147
+ r"renew\s*(my\s*)?passport|how\s*to\s*cook)\b",
148
+ re.IGNORECASE,
149
+ )
150
+
151
+ # App-flow routing is deliberately narrow. These rules only clarify the
152
+ # current BrgyLink registration flow; they run after the safety rules above
153
+ # and do not disclose account-specific information.
154
+ _SIGNUP_OTP_RE = re.compile(
155
+ r"(?=.*\b(?:otp|one\s*time\s*password|verification\s*code|email\s*code)\b)"
156
+ r"(?=.*\b(?:sign\s*up|signup|register|registration|mag-?register|rehistro|parehistro)\b)",
157
+ re.IGNORECASE,
158
+ )
159
+
160
+ _SIGNUP_RE = re.compile(
161
+ r"\b(sign\s*up|signup|register|registration|mag\s*register|"
162
+ r"rehistro|parehistro|agparehistro|magparehistro|panagparehistro)\b"
163
+ )
164
+ _CREATE_ACCOUNT_RE = re.compile(
165
+ r"\b(create|make|open|new|gumawa|gagawa|agaramid|manggawa)\b.{0,30}\b(?:resident\s*)?account\b|"
166
+ r"\baccount\b.{0,25}\b(create|make|open|new|gumawa)\b"
167
+ )
168
+ _VOTER_RE = re.compile(r"\b(voters?|vote|voting|botante|comelec|boto|eleksyon)\b")
169
+ _CODE_RE = re.compile(r"\b(otp|verification\s*code|email\s*code|code)\b")
170
+ _CODE_PROBLEM_RE = re.compile(
171
+ r"\b(expir\w*|incorrect|invalid|wrong|never|missing|resend|"
172
+ r"not\s*(?:receive\w*|arriv\w*)|did\s*not|didn\s*t|"
173
+ r"hindi|di\s*dumating|wala|naawat|awan|anggapo|mali|"
174
+ r"waiting|waited|does\s*not|doesn\s*t)\b"
175
+ )
176
+ _PASSWORD_RE = re.compile(r"\b(password|pass\s*word|pasword)\b")
177
+ _PASSWORD_PROBLEM_RE = re.compile(
178
+ r"\b(forgot\w*|forget\w*|lost|lose|losing|reset|recover\w*|nakalimut\w*|nalipat\w*|"
179
+ r"alingwan\w*|wrong|incorrect|invalid|mali|change|palitan)\b"
180
+ )
181
+ _ACCOUNT_RE = re.compile(r"\b(account|login|log\s*in|sign\s*in|sumrek|makapasok)\b")
182
+ _ACCOUNT_REVIEW_RE = re.compile(r"\b(pending|reject\w*|refus\w*|declin\w*|approv\w*|review|submitted|naisumite)\b")
183
+ _OFFICIALS_RE = re.compile(
184
+ r"\b(kapitan|captain|punong\s*barangay|kagawad|opisyal|officials?|"
185
+ r"barangay\s*(?:chairman|chairperson|council|leaders?)|councillors?|councilors?|tanod|sk\s*chairman)\b"
186
+ )
187
+ _BARANGAY_LOCATION_RE = re.compile(
188
+ r"\b(?:where\s*(?:is|are)|saan|nasaan|sadino|iner)\b.{0,30}"
189
+ r"\b(?:barangay|brgy)\b.{0,30}\bbagong\s*pag\s*asa\b|"
190
+ r"\bbagong\s*pag\s*asa\b.{0,30}\b(?:location|address|saan|nasaan|sadino|iner)\b"
191
+ )
192
+ _CAPABILITIES_RE = re.compile(
193
+ r"\b(?:what\s*(?:can|do)\s*(?:you|brgylink)|how\s*can\s*you\s*help|"
194
+ r"ano\s*(?:ang\s*)?(?:mga\s*)?(?:pwede|puwede|kaya)\s*(?:mong\s*)?gawin|"
195
+ r"anong\s*(?:mga\s*)?(?:tulong|serbisyo)\s*(?:ang\s*)?(?:kaya|pwede|puwede)|"
196
+ r"ania\s*(?:dagiti\s*)?(?:mabalin|tulong)|antoy\s*(?:saray\s*)?(?:tulong|sarag))\b"
197
+ )
198
+ _CIVIC_RE = re.compile(r"\b(sdg|civic\s*(?:tasks?|participation)|missions?|community\s*initiative)\b")
199
+ _EVENT_RE = re.compile(r"\b(events?|community\s*activities)\b")
200
+ _ID_RE = re.compile(r"\b(valid\s*id|government\s*(?:issued\s*)?id|passport|driver\w*\s*license)\b")
201
+ _ID_PHOTO_RE = re.compile(
202
+ r"(?=.*\b(?:id|identification|passport|licen[cs]e)\b)"
203
+ r"(?=.*\b(?:photos?|pictures?|upload\w*|attach\w*|image|readable|blurry|photograph\w*)\b)"
204
+ )
205
+ # Never confuse a request for private credentials or somebody else's records
206
+ # with help recovering the resident's own password. No records are accessed.
207
+ _PRIVATE_DATA_RE = re.compile(
208
+ r"\b(?:admin|administrator)\b.{0,30}\b(?:password|credentials|token|secret)\b|"
209
+ r"\b(?:password|credentials|token|secret)\b.{0,30}\b(?:admin|administrator)\b|"
210
+ r"\b(?:other|another|all)\s+residents?\b.{0,40}\b(?:ids?|records?|details|data)\b|"
211
+ r"\b(?:neighbou?r\w*|someone|somebody|another|other)\b.{0,40}\b(?:password|credentials|private\s*records)\b"
212
+ )
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Keyword-boost dictionary {pattern: {intent: boost_value}}
216
+ # ---------------------------------------------------------------------------
217
+ KEYWORD_BOOSTS: list[tuple[re.Pattern, dict]] = [
218
+ # Greetings
219
+ (re.compile(r"\b(hello|hi|hey|good\s*(morning|afternoon|evening)|kumusta|kamusta|naimbag|maabig|magandang)\b"), {"greeting": 0.50}),
220
+
221
+ # Goodbye & Farewell
222
+ (re.compile(r"\b(paalam|makaalis|agpakada|innakon|goodbye|bye\s*bye|quit|exit|signing\s*off)\b"), {"goodbye": 0.50}),
223
+ (re.compile(r"\b(salamat|maraming salamat|agyamanak|balbaleg)\b"), {"thanks": 0.25}),
224
+
225
+ # Document intents
226
+ (re.compile(r"\bclearance\b"), {"clearance": 0.30}),
227
+ (re.compile(r"\bindigenc[yi]\b"), {"indigency": 0.40}),
228
+ (re.compile(r"\bresidency\b"), {"residency": 0.35}),
229
+ (re.compile(r"\bkatibayan\s+na\s+nakatira\b"), {"residency": 0.60}),
230
+ (re.compile(r"\bproof\s+of\s+residen\w*\b"), {"residency": 0.55}),
231
+ (re.compile(r"\bgood moral\b(?!\s*morning)"), {"good_moral": 0.40}),
232
+ (re.compile(r"\bbarangay i\.?d\.?\b|\bbgy id\b"), {"barangay_id": 0.50}),
233
+ (re.compile(r"\bbusiness (clearance|permit)\b"), {"business_permit": 0.40}),
234
+ (re.compile(r"\b(negosyo|sari.?sari|tindahan)\b"), {"business_permit": 0.25}),
235
+
236
+ # Emergency (supplement safety pre-classifier)
237
+ (re.compile(r"\b(sunog|apoy|uram|fire|911|ambulance|ambulansya)\b"), {"emergency": 0.55}),
238
+ (re.compile(r"\b(saklolo|tulong.*tulong|aksidente|danger|peligro)\b"), {"emergency": 0.40}),
239
+
240
+ # Blotter / complaints / disputes
241
+ (re.compile(r"\b(blotter|reklamo|magreklamo|ireport|panagreklamo)\b"), {"report_incident": 0.40}),
242
+ (re.compile(r"\b(ingay|maingay|maingal|makapaingal|riri|away|nakawan|takew)\b"), {"report_incident": 0.35}),
243
+
244
+ # Lupon / Katarungang Pambarangay
245
+ (re.compile(r"\b(lupon|tagapamayapa|katarungang|mediation|conciliation|alitan|kolkol|kolkolan|pangkat)\b"), {"lupon": 0.50}),
246
+
247
+ # Assistance / Ayuda / AICS
248
+ (re.compile(r"\b(aics|ayuda|tulong.?salapi|tulong.?pinansyal|relief|4ps|pantawid|ponpon|burial)\b"), {"aics_assistance": 0.45}),
249
+
250
+ # Senior Citizen & Solo Parent
251
+ (re.compile(r"\b(senior citizen|osca|matatken|lallakay|babbaket)\b"), {"senior_citizen": 0.50}),
252
+ (re.compile(r"\b(solo parent|single parent|nag.?iisang magulang|agsolsolo a nagannak)\b"), {"solo_parent": 0.50}),
253
+
254
+ # Health (but not medical symptoms — those go to safety pre-classifier)
255
+ (re.compile(r"\b(health center|bhc|bakuna|vaccin\w*|prenatal|gamot|agas|bitamina|salun.?at)\b"), {"health_services": 0.45}),
256
+
257
+ # Garbage
258
+ (re.compile(r"\b(basura|garbage|trash|hakot|panangala na basura|panag.?ala ti basura|kolekta)\b"), {"garbage": 0.50}),
259
+
260
+ # Office hours / Location
261
+ (re.compile(r"\b(office hours|opening times|oras na opisina|oras ti opisina|tanggapan|schedule\s*(ng|na|ti)\s*opisina)\b|\b(?:bukas|open)\b.{0,25}\b(?:opisina|office|barangay hall)\b"), {"office_hours": 0.45}),
262
+ (re.compile(r"\b(where\s*(?:is|can\s*i\s*find)\s*(?:the\s*)?(?:office|barangay\s*hall)|nasaan\s*(ang\s*)?opisina|saan\s*(so|ti)\s*opisina|address|location\s*(of|ng|na)?\s*(the\s*)?(office|barangay)?)\b"), {"office_hours": 0.45}),
263
+
264
+ # Fees — must beat clearance when fee-related words present
265
+ (re.compile(r"\b(fees?|bayad|bayar|magkano|panpiga|mano ti bayad|mano so bayad|presyo|singil|cost|price|how\s*much)\b"), {"fees": 0.50}),
266
+
267
+ # Voter Registration
268
+ (re.compile(r"\b(voter|botante|comelec|rehistro.*boto|eleksyon)\b"), {"voter_registration": 0.50}),
269
+
270
+ # SDG / Community Initiatives
271
+ (re.compile(r"\b(sdg|community initiative|civic task|tree planting|clean.?up)\b"), {"sdg_mission": 0.50}),
272
+
273
+ # Officials
274
+ (re.compile(r"\b(kapitan|punong barangay|kagawad|opisyal|officials|tanod|sk chairman)\b"), {"officials": 0.45}),
275
+
276
+ # Status / Tracking
277
+ (re.compile(r"\b(status|track|ready for pickup|nasaan na|subaybayan|nabantayan)\b"), {"document_status": 0.30}),
278
+
279
+ # About App
280
+ (re.compile(r"\b(what is brgylink|ano ang brgylink|ania ti brgylink|antoy brgylink|features of brgylink)\b"), {"about_app": 0.50}),
281
+ ]
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Helper responses (non-factual UI prompts)
285
+ # ---------------------------------------------------------------------------
286
+ HELPER_TEXTS = {
287
+ "choose_document": {
288
+ "english": "Which document do you need? You can request a Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, or Barangay ID.",
289
+ "tagalog": "Anong dokumento po ang kailangan ninyo? Maaari kayong humiling ng Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, o Barangay ID.",
290
+ "ilocano": "Ania a dokumento ti kasapulam kabsat? Mabalin ti agkiddaw iti Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, wenno Barangay ID.",
291
+ "pangasinan": "Antoy dokumento ya kasapulan mo kabaleyan? Nayari kayon mangala na Barangay Clearance, Certificate of Indigency, Certificate of Residency, Business Clearance, Certificate of Good Moral Character, odino Barangay ID."
292
+ },
293
+ "status": {
294
+ "english": "Open Document Requests in BrgyLink to check the status of your application (Pending, Processing, Ready for Pickup, Completed, or Rejected). The chatbot cannot access personal request details.",
295
+ "tagalog": "Buksan ang Document Requests sa BrgyLink upang makita ang status ng iyong request (Pending, Processing, Ready for Pickup, Completed, o Rejected). Hindi maa-access ng chatbot ang personal na request details.",
296
+ "ilocano": "Lukatan ti Document Requests iti BrgyLink tapno makita ti kasasaad ti kineddawmo a dokumento (Pending, Processing, Ready for Pickup, Completed, wenno Rejected). Saan a maakses ti chatbot ti personal a request details.",
297
+ "pangasinan": "Lukatan so Document Requests ed BrgyLink pian nengnengen so status na kineddeng mon dokumento (Pending, Processing, Ready for Pickup, Completed, odino Rejected). Ag nayarin aksesan na chatbot so personal ya request details."
298
+ },
299
+ "legal_disclaimer": {
300
+ "english": "The chatbot cannot provide legal advice. For legal concerns, you may visit the barangay office to inquire about Lupon Tagapamayapa (mediation) if the dispute is within barangay jurisdiction, or consult a lawyer for matters beyond barangay scope.",
301
+ "tagalog": "Hindi makapagbigay ng legal advice ang chatbot. Para sa legal concerns, bumisita sa opisina ng barangay para sa Lupon Tagapamayapa (mediation) kung sakop ng barangay ang usapin, o kumonsulta sa abogado para sa mga bagay na lampas sa sakop ng barangay.",
302
+ "ilocano": "Saan a makaited ti chatbot iti legal advice. Para kadagiti legal concerns, bisitaem ti opisina ti barangay tapno umammo maipapan iti Lupon Tagapamayapa (mediation) no sakop ti barangay, wenno konsultaem ti abogado para kadagiti saan a sakop.",
303
+ "pangasinan": "Ag makaiter na legal advice so chatbot. Para ed saray legal concerns, bisitaen so opisina na barangay para ed Lupon Tagapamayapa (mediation) no sakop na barangay, odino konsultaen so abogado para ed saray agtaay ed sakop."
304
+ },
305
+ "unverified_disclaimer": {
306
+ "english": "\n\n(Note: This information is not verified. Please confirm with the barangay office.)",
307
+ "tagalog": "\n\n(Paalala: Ang impormasyong ito ay hindi pa verified. Pakikumpirma sa opisina ng barangay.)",
308
+ "ilocano": "\n\n(Pammalagip: Daytoy nga impormasion ket saan pay a verified. Pangngaasi a kumpirmaen iti opisina ti barangay.)",
309
+ "pangasinan": "\n\n(Paimano: Saya ya impormasyon et agni verified. Kumpirmaen ed opisina na barangay.)"
310
+ }
311
+ }
312
+
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # Knowledge Base Loader
316
+ # ---------------------------------------------------------------------------
317
+ _kb_cache: dict | None = None
318
+
319
+ def load_knowledge_base() -> dict:
320
+ """Load knowledge_base.json and index by intent."""
321
+ global _kb_cache
322
+ if _kb_cache is not None:
323
+ return _kb_cache
324
+ with open(KB_FILE, encoding="utf-8") as f:
325
+ kb = json.load(f)
326
+ index = {}
327
+ for svc in kb.get("services", []):
328
+ index[svc["intent"]] = svc
329
+ _kb_cache = index
330
+ return index
331
+
332
+
333
+ def get_kb_answer(intent: str, language: str, variant: str | None = None) -> str:
334
+ """Get the answer for an intent from the knowledge base.
335
+
336
+ Appends an unverified-data disclaimer only when content_type is
337
+ 'barangay_fact' and the entry is not validly verified.
338
+ System copy (greetings, fallback, etc.) never gets the disclaimer.
339
+ """
340
+ kb = load_knowledge_base()
341
+ entry = kb.get(intent)
342
+
343
+ def is_valid_answer(ans):
344
+ if not isinstance(ans, dict):
345
+ return False
346
+ for lang in ["english", "tagalog", "ilocano", "pangasinan"]:
347
+ if lang not in ans or not isinstance(ans[lang], str) or not ans[lang].strip():
348
+ return False
349
+ return True
350
+
351
+ if not entry or not is_valid_answer(entry.get("answer")):
352
+ # Fall through to out_of_scope
353
+ entry = kb.get("out_of_scope", {})
354
+
355
+ answer_obj = entry.get("answer", {})
356
+ # Variants are still curated KB content, never generated factual answers.
357
+ candidate = entry.get("answer_variants", {}).get(variant) if variant else None
358
+ if is_valid_answer(candidate):
359
+ answer_obj = candidate
360
+ text = answer_obj.get(language, answer_obj.get("english", ""))
361
+
362
+ # Only barangay_fact entries can carry the unverified disclaimer.
363
+ content_type = entry.get("content_type", "barangay_fact")
364
+ if content_type != "barangay_fact":
365
+ return text
366
+
367
+ import datetime
368
+
369
+ is_verified = entry.get("verified", False)
370
+
371
+ # Check verification requirements
372
+ if is_verified:
373
+ has_reqs = all(entry.get(f) for f in ["source", "verified_by", "verified_at", "expires_at"])
374
+ if not has_reqs:
375
+ is_verified = False
376
+ else:
377
+ try:
378
+ expiry_date = datetime.datetime.fromisoformat(entry["expires_at"].replace("Z", "+00:00"))
379
+ if datetime.datetime.now(datetime.timezone.utc) > expiry_date:
380
+ is_verified = False
381
+ except (ValueError, TypeError, AttributeError):
382
+ is_verified = False
383
+
384
+ if not is_verified:
385
+ disclaimer = HELPER_TEXTS["unverified_disclaimer"].get(language, HELPER_TEXTS["unverified_disclaimer"]["english"])
386
+ if disclaimer not in text:
387
+ text += disclaimer
388
+
389
+ return text
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # Text preprocessing
394
+ # ---------------------------------------------------------------------------
395
+ def apply_aliases(text: str) -> str:
396
+ for pattern, replacement in ALIASES:
397
+ text = pattern.sub(replacement, text)
398
+ return text
399
+
400
+
401
+ def normalize(text: str) -> str:
402
+ return re.sub(r"\s+", " ", re.sub(r"[^\w\s]", " ", text.lower())).strip()
403
+
404
+
405
+ def char_ngrams(text: str, ns=(2, 3, 4, 5)) -> Counter:
406
+ padded = f" {text} "
407
+ c = Counter()
408
+ for n in ns:
409
+ for i in range(len(padded) - n + 1):
410
+ c[("c", padded[i:i+n])] += 1
411
+ return c
412
+
413
+
414
+ def word_ngrams(tokens: list[str], ns=(1, 2)) -> Counter:
415
+ c = Counter()
416
+ for n in ns:
417
+ for i in range(len(tokens) - n + 1):
418
+ gram = " ".join(tokens[i:i+n])
419
+ c[("w", gram)] += 1
420
+ return c
421
+
422
+
423
+ def featurize(text: str) -> Counter:
424
+ norm = normalize(apply_aliases(normalize(text)))
425
+ tokens = norm.split()
426
+ features = char_ngrams(norm)
427
+ features.update(word_ngrams(tokens))
428
+ return features
429
+
430
+
431
+ # ---------------------------------------------------------------------------
432
+ # Scored Language Detection (Ilocano, Pangasinan, Tagalog, English)
433
+ # ---------------------------------------------------------------------------
434
+ # Short common tokens that overlap between Pangasinan and English.
435
+ # These get reduced weight to prevent English from being classified as Pangasinan.
436
+ _SHORT_PAN_TOKENS = {"ed", "so", "ya", "et", "to", "mi", "yo", "da", "ak", "la"}
437
+
438
+ _LANG_RULES: dict[str, list[tuple[re.Pattern, float]]] = {
439
+ "ilocano": [
440
+ (re.compile(r"\b(dagiti|kadagiti|wenno|ket|tapno|ken|nga|met|pay|laeng|amin|ditoy|daytoy|kaniak|kenka|kadakayo|datayo|dakami|isuda|kasta)\b"), 1.4),
441
+ (re.compile(r"\b(mabalin|agkiddaw|kasano|wen|saan|apay|sadino|kaano|kaanu|mano ti|anian|ania|anya|masapulko|masapul)\b"), 2.5),
442
+ (re.compile(r"\b(ilocano|ilokano|siasino|nalipatak|makaanges|ubing|kabsat|kailian|agas|ubbing|kolkol|riri|uram|panagbiag|malungsot|agyamanak|agpakada|innakon|agsapa)\b"), 3.5),
443
+ (re.compile(r"\b(naimbag|aldaw|bigat|malem|rabii|dios ti agngina|makasungbatak|makaawat)\b"), 3.5),
444
+ (re.compile(r"\b(mangited|mangaramid|pangngeddeng|panangsalaysay|lukatan|piliem|isumitem|panagkiddaw)\b"), 2.0),
445
+ # iti/ti get reduced weight to avoid false positives on short text
446
+ (re.compile(r"\biti\b"), 0.6),
447
+ (re.compile(r"\bti\b"), 0.4),
448
+ ],
449
+ "pangasinan": [
450
+ # High-confidence Pangasinan markers
451
+ (re.compile(r"\b(saray|diad|onla|odino|pian|kabaleyan|natan)\b"), 2.5),
452
+ (re.compile(r"\b(mano so|panon so|antoy|anto|panon|kapigan|kasapulan|amtaen|ugugaw|makapaingal|alitan|kolkolan|apoy|ponpon|mabiin|kailangan koy|panpiga|piga)\b"), 3.2),
453
+ (re.compile(r"\b(pangasinan|anggapo|agko|siopa|ugaw|makainawa|makatalos|anggad|nayarin|manggawa|man.?ingat|makaalis|balbaleg|maabig|kabuasan|ngarem|agew|labi|masantos)\b"), 3.5),
454
+ (re.compile(r"\b(say|inkuan|nanengneng|ipaliwawa|piliyen|mangikeddeng|nengnengen|silpin)\b"), 2.0),
455
+ # Pangasinan pronoun 'ak' (first-person) — medium weight
456
+ (re.compile(r"\bak\b"), 1.5),
457
+ # Short tokens that also appear in English — low weight
458
+ (re.compile(r"\b(ed|so|ya|et|tan|met|labat)\b"), 0.3),
459
+ ],
460
+ "tagalog": [
461
+ (re.compile(r"\b(ang|ng|mga|sa|ay|ko|mo|po|opo|naman|lang|ba|dito|ito|yan|yun|natin|ninyo|amin|atin|nila|siya|sila)\b"), 0.8),
462
+ (re.compile(r"\b(magandang|kumusta|kamusta|salamat|paalam|sige|walang anuman|pasensya|maraming salamat|ingat po)\b"), 2.0),
463
+ (re.compile(r"\b(paano|anong|ano|saan|kailan|sino|bakit|hindi|yung|maling|kailangan|gusto|mayroon|magkano|bayad|libre|puwede|maaari)\b"), 2.0),
464
+ (re.compile(r"\b(tagalog|filipino|pilipino|kumuha|makuha|humiling|ireport|magreklamo|kapitan|kagawad|tanod|nakatira)\b"), 3.0),
465
+ ],
466
+ }
467
+
468
+
469
+ def detect_language(message: str) -> str:
470
+ """Detect whether text is Ilocano, Pangasinan, Tagalog, or English.
471
+
472
+ Uses a scoring system with safety checks against false positives
473
+ from short overlapping tokens (e.g. 'to', 'so', 'ed' which appear
474
+ in both English and Pangasinan).
475
+ """
476
+ value = normalize(message)
477
+ tokens = set(value.split())
478
+
479
+ scores = {lang: 0.0 for lang in _LANG_RULES}
480
+ match_counts = {lang: 0 for lang in _LANG_RULES}
481
+
482
+ for lang, rules in _LANG_RULES.items():
483
+ for pattern, weight in rules:
484
+ matches = len(pattern.findall(value))
485
+ if matches > 0:
486
+ scores[lang] += matches * weight
487
+ match_counts[lang] += matches
488
+
489
+ best_lang = max(scores, key=scores.__getitem__)
490
+ best_score = scores[best_lang]
491
+
492
+ # Require a minimum threshold
493
+ if best_score <= 0.5:
494
+ return "english"
495
+
496
+ # Safety: If Pangasinan won but ALL matching tokens are short/ambiguous,
497
+ # and there are no high-confidence Pangasinan markers, default to English.
498
+ if best_lang == "pangasinan":
499
+ pan_only_short = tokens.issubset(
500
+ _SHORT_PAN_TOKENS | {t for t in tokens if len(t) <= 3}
501
+ | set(value.split())
502
+ )
503
+ # Check if any high-confidence Pangasinan word is present
504
+ high_conf_pan = re.search(
505
+ r"\b(saray|diad|onla|odino|pian|kabaleyan|pangasinan|makatalos|"
506
+ r"maabig|kabuasan|balbaleg|antoy|panon|kapigan|nayarin|mangikeddeng|"
507
+ r"nengnengen|say|anggad|ugugaw|kolkolan|panpiga|mabiin)\b",
508
+ value,
509
+ )
510
+ if not high_conf_pan and best_score < 2.0:
511
+ return "english"
512
+
513
+ return best_lang
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # Language switch intent detection
518
+ # ---------------------------------------------------------------------------
519
+ _LANG_SWITCH = re.compile(
520
+ r"(sagot|sumagot|reply|response|respond|speak|salita|wika|sagutin|usaren|pansalita|answer|isagot).{0,30}"
521
+ r"(tagalog|filipino|ilocano|ilokano|pangasinan|english|ingles)"
522
+ r"|(tagalog|filipino|ilocano|ilokano|pangasinan|english|ingles).{0,30}"
523
+ r"(ang|lang|only|please|lamang|na lang|so usaren|ti usarem)",
524
+ re.IGNORECASE,
525
+ )
526
+
527
+ _LANG_SWITCH_ENGLISH = re.compile(
528
+ r"(answer|reply|speak|respond|talk).{0,20}(in\s+)?(english|ingles)"
529
+ r"|(english|ingles).{0,20}(please|lang|only|po)",
530
+ re.IGNORECASE,
531
+ )
532
+
533
+ def _extract_switch_language(message: str) -> str | None:
534
+ """Extract the target language from a language-switch request."""
535
+ value = message.lower()
536
+ if _LANG_SWITCH_ENGLISH.search(value):
537
+ return "english"
538
+ m = _LANG_SWITCH.search(value)
539
+ if not m:
540
+ return None
541
+ full = m.group(0).lower()
542
+ if "ilocano" in full or "ilokano" in full:
543
+ return "ilocano"
544
+ if "pangasinan" in full:
545
+ return "pangasinan"
546
+ if "tagalog" in full or "filipino" in full:
547
+ return "tagalog"
548
+ if "english" in full or "ingles" in full:
549
+ return "english"
550
+ return None
551
+
552
+
553
+ def language_switch(message: str) -> bool:
554
+ return _extract_switch_language(message) is not None
555
+
556
+
557
+ # ---------------------------------------------------------------------------
558
+ # Training
559
+ # ---------------------------------------------------------------------------
560
+ def train() -> dict:
561
+ with open(INTENTS_FILE, "rb") as f:
562
+ training_bytes = f.read()
563
+ intents = json.loads(training_bytes)["intents"]
564
+
565
+ docs = [(item["tag"], pattern) for item in intents for pattern in item["patterns"]]
566
+ freq = Counter()
567
+ features_list = []
568
+ for _, text in docs:
569
+ fv = featurize(text)
570
+ features_list.append(fv)
571
+ freq.update(fv.keys())
572
+
573
+ N = len(docs)
574
+ idf = {k: math.log((1 + N) / (1 + cnt)) + 1 for k, cnt in freq.items()}
575
+
576
+ centroids = defaultdict(Counter)
577
+ counts = Counter()
578
+ for (tag, _), fv in zip(docs, features_list):
579
+ counts[tag] += 1
580
+ for k, v in fv.items():
581
+ centroids[tag][k] += v * idf.get(k, 1)
582
+
583
+ for tag in centroids:
584
+ n = counts[tag]
585
+ for k in centroids[tag]:
586
+ centroids[tag][k] /= n
587
+
588
+ model = {
589
+ "version": MODEL_VERSION,
590
+ "training_sha256": hashlib.sha256(training_bytes).hexdigest(),
591
+ "idf": idf,
592
+ "centroids": dict(centroids),
593
+ }
594
+ # An interrupted training run must not replace a working model with a
595
+ # partially written pickle. Save alongside the target and atomically swap.
596
+ fd, temporary_path = tempfile.mkstemp(prefix=".classifier-", dir=os.path.dirname(MODEL_FILE))
597
+ try:
598
+ with os.fdopen(fd, "wb") as f:
599
+ pickle.dump(model, f)
600
+ os.replace(temporary_path, MODEL_FILE)
601
+ finally:
602
+ if os.path.exists(temporary_path):
603
+ os.unlink(temporary_path)
604
+ return model
605
+
606
+
607
+ def load_model() -> dict:
608
+ if not os.path.exists(MODEL_FILE):
609
+ return train()
610
+ try:
611
+ with open(MODEL_FILE, "rb") as f:
612
+ model = pickle.load(f)
613
+ except (pickle.UnpicklingError, EOFError, ValueError, AttributeError):
614
+ return train()
615
+ with open(INTENTS_FILE, "rb") as f:
616
+ fingerprint = hashlib.sha256(f.read()).hexdigest()
617
+ if (not isinstance(model, dict) or model.get("version") != MODEL_VERSION
618
+ or model.get("training_sha256") != fingerprint
619
+ or not isinstance(model.get("idf"), dict)
620
+ or not isinstance(model.get("centroids"), dict) or not model["centroids"]):
621
+ return train()
622
+ return model
623
+
624
+
625
+ def cosine(left: Counter, right: dict) -> float:
626
+ dot = sum(v * right.get(k, 0) for k, v in left.items())
627
+ a = math.sqrt(sum(x * x for x in left.values()))
628
+ b = math.sqrt(sum(x * x for x in right.values()))
629
+ return dot / (a * b) if a and b else 0.0
630
+
631
+
632
+ def compute_boost(value: str) -> dict[str, float]:
633
+ boost = defaultdict(float)
634
+ for pattern, intent_boosts in KEYWORD_BOOSTS:
635
+ if pattern.search(value):
636
+ for intent, amount in intent_boosts.items():
637
+ boost[intent] += amount
638
+ return boost
639
+
640
+
641
+ def document_in(message: str) -> str | None:
642
+ value = normalize(apply_aliases(normalize(message)))
643
+ # Don't match "cert" alone — too ambiguous
644
+ return next((doc for key, doc in DOCUMENTS.items() if key != "cert" and key in value), None)
645
+
646
+
647
+ def new_session() -> dict:
648
+ return {
649
+ "preferred_language": None,
650
+ "pending": None,
651
+ "selected_document": None,
652
+ "last_intent": None,
653
+ "turn_count": 0,
654
+ }
655
+
656
+
657
+ def classify(message: str, model: dict) -> tuple[str, float, float]:
658
+ """Classify a message. Returns (intent, score, margin).
659
+
660
+ margin is the gap between the top intent and the runner-up.
661
+ """
662
+ fv = featurize(message)
663
+ vec = {k: v * model["idf"].get(k, 1) for k, v in fv.items()}
664
+ boost = compute_boost(normalize(message))
665
+
666
+ ranked = sorted(
667
+ (
668
+ (tag, cosine(vec, centroid) + boost.get(tag, 0))
669
+ for tag, centroid in model["centroids"].items()
670
+ ),
671
+ key=lambda x: x[1],
672
+ reverse=True,
673
+ )
674
+ intent, score = ranked[0]
675
+ runner_up_score = ranked[1][1] if len(ranked) > 1 else 0.0
676
+ margin = score - runner_up_score
677
+
678
+ # Strict threshold + margin requirement
679
+ if score < CONFIDENCE_THRESHOLD or margin < MARGIN_THRESHOLD:
680
+ return FALLBACK, score, margin
681
+
682
+ return intent, score, margin
683
+
684
+
685
+ def _build(intent: str, score: float, language: str, response: str, session: dict) -> dict:
686
+ session["last_intent"] = intent
687
+ session["turn_count"] = session.get("turn_count", 0) + 1
688
+ return {
689
+ "intent": intent,
690
+ "similarity": round(score, 4),
691
+ "language": language,
692
+ "response": response,
693
+ "session": session,
694
+ }
695
+
696
+
697
+ # ---------------------------------------------------------------------------
698
+ # Main message handler
699
+ # ---------------------------------------------------------------------------
700
+ def handle_message(message: str, session: dict | None = None, model: dict | None = None) -> dict:
701
+ session = session or new_session()
702
+ model = model or load_model()
703
+ value = normalize(message)
704
+
705
+ # Reset command
706
+ if value in {"reset", "restart", "clear", "start over"}:
707
+ session.clear()
708
+ session.update(new_session())
709
+ return _build("reset", 1.0, "english", "Conversation reset.", session)
710
+
711
+ # --- Language detection ---
712
+ detected = detect_language(message)
713
+ language = session.get("preferred_language") or detected
714
+
715
+ # Language switch request
716
+ switch_lang = _extract_switch_language(message)
717
+ if switch_lang:
718
+ language = switch_lang
719
+ session["preferred_language"] = language
720
+
721
+ # ===================================================================
722
+ # SAFETY PRE-CLASSIFIER — always runs first, overrides everything
723
+ # ===================================================================
724
+
725
+ # 1. Emergency (life-threatening)
726
+ if _EMERGENCY_RE.search(value):
727
+ resp = get_kb_answer("emergency", language)
728
+ return _build("emergency", 1.0, language, resp, session)
729
+
730
+ # 2. Threats, violence, abuse, self-harm
731
+ if _THREAT_RE.search(value):
732
+ resp = get_kb_answer("safety_threat", language)
733
+ return _build("safety_threat", 1.0, language, resp, session)
734
+
735
+ # 3. Medical symptoms (non-emergency) — route to health guidance
736
+ if _MEDICAL_SYMPTOM_RE.search(value):
737
+ # Check if also emergency-level
738
+ resp = get_kb_answer("medical_non_emergency", language)
739
+ return _build("medical_non_emergency", 1.0, language, resp, session)
740
+
741
+ # 4. Legal questions
742
+ if _LEGAL_RE.search(value):
743
+ resp = HELPER_TEXTS["legal_disclaimer"][language]
744
+ return _build("legal_disclaimer", 1.0, language, resp, session)
745
+
746
+ # 5. Obvious out-of-scope topics
747
+ # Passport/licence *upload* guidance for signup is in scope, not advice
748
+ # about obtaining or renewing those national documents.
749
+ signup_id = bool((_SIGNUP_RE.search(value) or _CREATE_ACCOUNT_RE.search(value)) and _ID_RE.search(value))
750
+ national_document_request = re.search(
751
+ r"\b(renew|renewal|apply\s*for|obtain|get\s*(?:a|my))\b.{0,25}\b(passport|license)\b|"
752
+ r"\b(passport|license)\b.{0,25}\b(renew|renewal)\b", value
753
+ )
754
+ scope_value = _ID_RE.sub("", value) if signup_id and not national_document_request else value
755
+ if _PRIVATE_DATA_RE.search(value) or _OUT_OF_SCOPE_RE.search(scope_value):
756
+ resp = get_kb_answer("out_of_scope", language)
757
+ return _build("out_of_scope", 1.0, language, resp, session)
758
+
759
+ # Explicit app concepts resolve common short wording without lowering the
760
+ # classifier's uncertainty threshold. Recovery wins over signup timing.
761
+ if _CODE_RE.search(value) and _CODE_PROBLEM_RE.search(value):
762
+ return _build("account_help", 0.98, language, get_kb_answer("account_help", language, "otp"), session)
763
+ if _PASSWORD_RE.search(value) and (_PASSWORD_PROBLEM_RE.search(value) or re.search(r"\bagko\s*amta\b", value)):
764
+ return _build("account_help", 0.98, language, get_kb_answer("account_help", language, "password"), session)
765
+ signup_question = bool(_SIGNUP_RE.search(value) or _CREATE_ACCOUNT_RE.search(value))
766
+ if (_ACCOUNT_RE.search(value) or signup_question) and _ACCOUNT_REVIEW_RE.search(value):
767
+ return _build("account_help", 0.98, language, get_kb_answer("account_help", language, "review"), session)
768
+ if _ACCOUNT_RE.search(value) and not signup_question:
769
+ variant = "review" if _ACCOUNT_REVIEW_RE.search(value) else None
770
+ return _build("account_help", 0.98, language, get_kb_answer("account_help", language, variant), session)
771
+ if _EVENT_RE.search(value):
772
+ return _build("events", 0.98, language, get_kb_answer("events", language), session)
773
+ if _CIVIC_RE.search(value):
774
+ return _build("sdg_mission", 0.98, language, get_kb_answer("sdg_mission", language), session)
775
+ if _OFFICIALS_RE.search(value):
776
+ return _build("officials", 0.98, language, get_kb_answer("officials", language), session)
777
+ if _BARANGAY_LOCATION_RE.search(value):
778
+ return _build("office_hours", 0.98, language, get_kb_answer("office_hours", language), session)
779
+ if _CAPABILITIES_RE.search(value):
780
+ return _build("about_app", 0.98, language, get_kb_answer("about_app", language), session)
781
+ if _SIGNUP_RE.search(value) and _VOTER_RE.search(value):
782
+ return _build("voter_registration", 0.98, language, get_kb_answer("voter_registration", language), session)
783
+ if _SIGNUP_OTP_RE.search(value) and not _VOTER_RE.search(value):
784
+ resp = get_kb_answer("registration", language)
785
+ return _build("registration", 0.98, language, resp, session)
786
+ if signup_question and not _VOTER_RE.search(value):
787
+ variant = "id" if _ID_RE.search(value) else None
788
+ return _build("registration", 0.98, language, get_kb_answer("registration", language, variant), session)
789
+ if _ID_RE.search(value) and re.search(r"\b(?:\d{1,2}|minor|age)\b", value):
790
+ return _build("registration", 0.98, language, get_kb_answer("registration", language, "id"), session)
791
+ if _ID_PHOTO_RE.search(value):
792
+ return _build("registration", 0.98, language, get_kb_answer("registration", language, "id"), session)
793
+
794
+ # ===================================================================
795
+ # COMPOUND QUERY DETECTION
796
+ # ===================================================================
797
+
798
+ # "How much for clearance?" / "bayad sa clearance" → fees, not clearance
799
+ _COMPOUND_FEE_RE = re.compile(
800
+ r"\b(fees?|bayad|bayar|magkano|panpiga|cost|price|singil|how\s*much|mano)\b"
801
+ r".{0,30}\b(clearance|indigency|residency|good moral|barangay id|business|permit|certificate|sertipiko|dokumento)\b",
802
+ re.IGNORECASE,
803
+ )
804
+ _COMPOUND_FEE_RE2 = re.compile(
805
+ r"\b(clearance|indigency|residency|good moral|barangay id|business|permit|certificate|sertipiko|dokumento)\b"
806
+ r".{0,30}\b(fees?|bayad|bayar|magkano|panpiga|cost|price|singil|how\s*much|mano)\b",
807
+ re.IGNORECASE,
808
+ )
809
+ if _COMPOUND_FEE_RE.search(value) or _COMPOUND_FEE_RE2.search(value):
810
+ doc = document_in(message)
811
+ if doc:
812
+ session["selected_document"] = doc
813
+ resp = get_kb_answer("fees", language)
814
+ return _build("fees", 0.95, language, resp, session)
815
+
816
+ # "clearance for business" → business_permit
817
+ if re.search(r"\bclearance\b.{0,20}\bbusiness\b|\bbusiness\b.{0,20}\bclearance\b", value):
818
+ resp = get_kb_answer("business_permit", language)
819
+ return _build("business_permit", 0.95, language, resp, session)
820
+
821
+ # ===================================================================
822
+ # TF-IDF CLASSIFIER
823
+ # ===================================================================
824
+ intent, score, margin = classify(message, model)
825
+
826
+ # Blotter status check
827
+ if re.search(r"\b(blotter|reklamo|complaint|incident report)\b", value) and \
828
+ re.search(r"\b(status|track|history|nasaan|naitala|in-process|update|naasikaso|pakaamta)\b", value):
829
+ resp = get_kb_answer("blotter_status", language)
830
+ return _build("blotter_status", score, language, resp, session)
831
+
832
+ # Document status check — but only if the message is clearly about status
833
+ _STATUS_RE = re.compile(
834
+ r"\b(status|track|ready for pickup|nasaan na|pending ba|approved na|processing na|rejected|"
835
+ r"kailan makukuha|kailan maaayos|done na|naaprubaran|subaybayan|nabantayan)\b",
836
+ re.IGNORECASE,
837
+ )
838
+ _DOC_OR_NAME_RE = re.compile(
839
+ r"\b(document|dokumento|request|hiniling|kineddaw|inkerew|"
840
+ r"clearance|indigency|residency|business|good\s*moral|barangay\s*id|sertipiko|certificate)\b",
841
+ re.IGNORECASE,
842
+ )
843
+ if _STATUS_RE.search(value) and (intent == "document_status" or _DOC_OR_NAME_RE.search(value)):
844
+ resp = get_kb_answer("document_status", language)
845
+ return _build("document_status", score, language, resp, session)
846
+
847
+ # Single-word / short document direct route
848
+ document = document_in(message)
849
+ if document and len(value.split()) <= 4 and not session.get("pending"):
850
+ session["selected_document"] = document
851
+ doc_intent = DOC_TO_INTENT.get(document, "document_request")
852
+ resp = get_kb_answer(doc_intent, language)
853
+ return _build(doc_intent, 1.0, language, resp, session)
854
+
855
+ # Resolve pending document choice
856
+ if session.get("pending") == "document_choice" and document:
857
+ session["pending"] = None
858
+ session["selected_document"] = document
859
+ doc_intent = DOC_TO_INTENT.get(document, "document_request")
860
+ resp = get_kb_answer(doc_intent, language)
861
+ return _build(doc_intent, 1.0, language, resp, session)
862
+
863
+ # Lupon mediation
864
+ if re.search(r"\b(lupon|katarungang pambarangay|mediation|pangkat|alitan|kolkol|kolkolan)\b", value):
865
+ resp = get_kb_answer("lupon", language)
866
+ return _build("lupon", score, language, resp, session)
867
+
868
+ # Generic document request
869
+ generic_doc = bool(re.search(
870
+ r"\b(file|apply|request|submit|humiling|mag.?file|magfile|kailangan|masapul|kasapulan|mangikeddeng)\b.{0,30}\b(document|dokumento|certificate|sertipiko|cert)\b"
871
+ r"|\b(document|dokumento|certificate|sertipiko)\b.{0,30}\b(file|apply|request|submit|kailangan|masapul|mangikeddeng)\b",
872
+ value,
873
+ ))
874
+ if generic_doc and not document:
875
+ session["pending"] = "document_choice"
876
+ return _build("document_request", score, language, HELPER_TEXTS["choose_document"][language], session)
877
+
878
+ # Named document mentioned
879
+ if document and intent not in {"fees", FALLBACK}:
880
+ session["selected_document"] = document
881
+ doc_intent = DOC_TO_INTENT.get(document, intent)
882
+ resp = get_kb_answer(doc_intent, language)
883
+ return _build(doc_intent, score, language, resp, session)
884
+
885
+ # ===================================================================
886
+ # ANSWER FROM KNOWLEDGE BASE
887
+ # ===================================================================
888
+ if switch_lang and intent in {FALLBACK, "fallback", "language_support"}:
889
+ intent = "language_support"
890
+ elif intent == "fallback":
891
+ intent = FALLBACK
892
+ resp = get_kb_answer(intent, language)
893
+ return _build(intent, score, language, resp, session)
894
+
895
+
896
+ if __name__ == "__main__":
897
+ m = train()
898
+ total_patterns = sum(
899
+ len(i["patterns"])
900
+ for i in json.load(open(INTENTS_FILE, encoding="utf-8"))["intents"]
901
+ )
902
+ print(
903
+ f"BrgyLink Smart Classifier v{MODEL_VERSION} (prototype) trained -> {MODEL_FILE}\n"
904
+ f" Intents : {len(m['centroids'])}\n"
905
+ f" Patterns: {total_patterns}\n"
906
+ f" Features: {len(m['idf'])} unique TF-IDF keys\n"
907
+ f" Languages: Pangasinan, Ilocano, Tagalog, English\n"
908
+ f" Note: This is a prototype. Answers sourced from knowledge_base.json."
909
+ )
test_app.py CHANGED
@@ -71,6 +71,7 @@ class AIServerTests(unittest.TestCase):
71
  service.app.config['TESTING'] = True
72
  service.anticheat = ReadyAntiCheat()
73
  service.predictor = ReadyPredictor()
 
74
  self.client = service.app.test_client()
75
  self.headers = {'Authorization': 'Bearer test-service-token'}
76
 
@@ -160,6 +161,27 @@ class AIServerTests(unittest.TestCase):
160
  self.assertEqual(response.status_code, 200)
161
  self.assertEqual(service.anticheat.register_calls, 0)
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  if __name__ == '__main__':
165
  unittest.main()
 
71
  service.app.config['TESTING'] = True
72
  service.anticheat = ReadyAntiCheat()
73
  service.predictor = ReadyPredictor()
74
+ service.chatbot.clear_sessions()
75
  self.client = service.app.test_client()
76
  self.headers = {'Authorization': 'Bearer test-service-token'}
77
 
 
161
  self.assertEqual(response.status_code, 200)
162
  self.assertEqual(service.anticheat.register_calls, 0)
163
 
164
+ def test_chat_requires_service_token_and_preserves_safety_overrides(self):
165
+ self.assertEqual(self.client.post('/chat', json={'message': 'hello'}).status_code, 401)
166
+ response = self.client.post(
167
+ '/chat',
168
+ headers=self.headers,
169
+ json={'message': 'Speak English, my baby cannot breathe'},
170
+ )
171
+ self.assertEqual(response.status_code, 200)
172
+ payload = response.get_json()
173
+ self.assertEqual(payload['intent'], 'emergency')
174
+ self.assertEqual(payload['language'], 'english')
175
+ self.assertNotIn('details', payload)
176
+
177
+ def test_chat_rejects_invalid_payloads_without_affecting_predict(self):
178
+ self.assertEqual(self.client.post('/chat', headers=self.headers, json={}).status_code, 400)
179
+ self.assertEqual(
180
+ self.client.post('/chat', headers=self.headers, json={'message': 'x' * 2001}).status_code,
181
+ 400,
182
+ )
183
+ self.assertEqual(self.post_image().status_code, 200)
184
+
185
 
186
  if __name__ == '__main__':
187
  unittest.main()