spagestic commited on
Commit
3457504
·
1 Parent(s): 8cd7871

Add update_globe tool to drive map markers and camera

Browse files

Let the agent highlight destinations on the MapLibre globe via tool calls,
with country centroids and globe state wired through ChatInterface outputs.

assets/country_centroids.json ADDED
@@ -0,0 +1,1252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "AW": {
3
+ "name": "Aruba",
4
+ "lat": 12.5,
5
+ "lng": -69.96666666
6
+ },
7
+ "AF": {
8
+ "name": "Afghanistan",
9
+ "lat": 33,
10
+ "lng": 65
11
+ },
12
+ "AO": {
13
+ "name": "Angola",
14
+ "lat": -12.5,
15
+ "lng": 18.5
16
+ },
17
+ "AI": {
18
+ "name": "Anguilla",
19
+ "lat": 18.25,
20
+ "lng": -63.16666666
21
+ },
22
+ "AX": {
23
+ "name": "Åland Islands",
24
+ "lat": 60.116667,
25
+ "lng": 19.9
26
+ },
27
+ "AL": {
28
+ "name": "Albania",
29
+ "lat": 41,
30
+ "lng": 20
31
+ },
32
+ "AD": {
33
+ "name": "Andorra",
34
+ "lat": 42.5,
35
+ "lng": 1.5
36
+ },
37
+ "AE": {
38
+ "name": "United Arab Emirates",
39
+ "lat": 24,
40
+ "lng": 54
41
+ },
42
+ "AR": {
43
+ "name": "Argentina",
44
+ "lat": -34,
45
+ "lng": -64
46
+ },
47
+ "AM": {
48
+ "name": "Armenia",
49
+ "lat": 40,
50
+ "lng": 45
51
+ },
52
+ "AS": {
53
+ "name": "American Samoa",
54
+ "lat": -14.33333333,
55
+ "lng": -170
56
+ },
57
+ "AQ": {
58
+ "name": "Antarctica",
59
+ "lat": -90,
60
+ "lng": 0
61
+ },
62
+ "TF": {
63
+ "name": "French Southern and Antarctic Lands",
64
+ "lat": -49.25,
65
+ "lng": 69.167
66
+ },
67
+ "AG": {
68
+ "name": "Antigua and Barbuda",
69
+ "lat": 17.05,
70
+ "lng": -61.8
71
+ },
72
+ "AU": {
73
+ "name": "Australia",
74
+ "lat": -27,
75
+ "lng": 133
76
+ },
77
+ "AT": {
78
+ "name": "Austria",
79
+ "lat": 47.33333333,
80
+ "lng": 13.33333333
81
+ },
82
+ "AZ": {
83
+ "name": "Azerbaijan",
84
+ "lat": 40.5,
85
+ "lng": 47.5
86
+ },
87
+ "BI": {
88
+ "name": "Burundi",
89
+ "lat": -3.5,
90
+ "lng": 30
91
+ },
92
+ "BE": {
93
+ "name": "Belgium",
94
+ "lat": 50.83333333,
95
+ "lng": 4
96
+ },
97
+ "BJ": {
98
+ "name": "Benin",
99
+ "lat": 9.5,
100
+ "lng": 2.25
101
+ },
102
+ "BF": {
103
+ "name": "Burkina Faso",
104
+ "lat": 13,
105
+ "lng": -2
106
+ },
107
+ "BD": {
108
+ "name": "Bangladesh",
109
+ "lat": 24,
110
+ "lng": 90
111
+ },
112
+ "BG": {
113
+ "name": "Bulgaria",
114
+ "lat": 43,
115
+ "lng": 25
116
+ },
117
+ "BH": {
118
+ "name": "Bahrain",
119
+ "lat": 26,
120
+ "lng": 50.55
121
+ },
122
+ "BS": {
123
+ "name": "Bahamas",
124
+ "lat": 24.25,
125
+ "lng": -76
126
+ },
127
+ "BA": {
128
+ "name": "Bosnia and Herzegovina",
129
+ "lat": 44,
130
+ "lng": 18
131
+ },
132
+ "BL": {
133
+ "name": "Saint Barthélemy",
134
+ "lat": 18.5,
135
+ "lng": -63.41666666
136
+ },
137
+ "SH": {
138
+ "name": "Saint Helena, Ascension and Tristan da Cunha",
139
+ "lat": -15.95,
140
+ "lng": -5.72
141
+ },
142
+ "BY": {
143
+ "name": "Belarus",
144
+ "lat": 53,
145
+ "lng": 28
146
+ },
147
+ "BZ": {
148
+ "name": "Belize",
149
+ "lat": 17.25,
150
+ "lng": -88.75
151
+ },
152
+ "BM": {
153
+ "name": "Bermuda",
154
+ "lat": 32.33333333,
155
+ "lng": -64.75
156
+ },
157
+ "BO": {
158
+ "name": "Bolivia",
159
+ "lat": -17,
160
+ "lng": -65
161
+ },
162
+ "BQ": {
163
+ "name": "Caribbean Netherlands",
164
+ "lat": 12.18,
165
+ "lng": -68.25
166
+ },
167
+ "BR": {
168
+ "name": "Brazil",
169
+ "lat": -10,
170
+ "lng": -55
171
+ },
172
+ "BB": {
173
+ "name": "Barbados",
174
+ "lat": 13.16666666,
175
+ "lng": -59.53333333
176
+ },
177
+ "BN": {
178
+ "name": "Brunei",
179
+ "lat": 4.5,
180
+ "lng": 114.66666666
181
+ },
182
+ "BT": {
183
+ "name": "Bhutan",
184
+ "lat": 27.5,
185
+ "lng": 90.5
186
+ },
187
+ "BV": {
188
+ "name": "Bouvet Island",
189
+ "lat": -54.43333333,
190
+ "lng": 3.4
191
+ },
192
+ "BW": {
193
+ "name": "Botswana",
194
+ "lat": -22,
195
+ "lng": 24
196
+ },
197
+ "CF": {
198
+ "name": "Central African Republic",
199
+ "lat": 7,
200
+ "lng": 21
201
+ },
202
+ "CA": {
203
+ "name": "Canada",
204
+ "lat": 60,
205
+ "lng": -95
206
+ },
207
+ "CC": {
208
+ "name": "Cocos (Keeling) Islands",
209
+ "lat": -12.5,
210
+ "lng": 96.83333333
211
+ },
212
+ "CH": {
213
+ "name": "Switzerland",
214
+ "lat": 47,
215
+ "lng": 8
216
+ },
217
+ "CL": {
218
+ "name": "Chile",
219
+ "lat": -30,
220
+ "lng": -71
221
+ },
222
+ "CN": {
223
+ "name": "China",
224
+ "lat": 35,
225
+ "lng": 105
226
+ },
227
+ "CI": {
228
+ "name": "Ivory Coast",
229
+ "lat": 8,
230
+ "lng": -5
231
+ },
232
+ "CM": {
233
+ "name": "Cameroon",
234
+ "lat": 6,
235
+ "lng": 12
236
+ },
237
+ "CD": {
238
+ "name": "DR Congo",
239
+ "lat": 0,
240
+ "lng": 25
241
+ },
242
+ "CG": {
243
+ "name": "Congo",
244
+ "lat": -1,
245
+ "lng": 15
246
+ },
247
+ "CK": {
248
+ "name": "Cook Islands",
249
+ "lat": -21.23333333,
250
+ "lng": -159.76666666
251
+ },
252
+ "CO": {
253
+ "name": "Colombia",
254
+ "lat": 4,
255
+ "lng": -72
256
+ },
257
+ "KM": {
258
+ "name": "Comoros",
259
+ "lat": -12.16666666,
260
+ "lng": 44.25
261
+ },
262
+ "CV": {
263
+ "name": "Cape Verde",
264
+ "lat": 16,
265
+ "lng": -24
266
+ },
267
+ "CR": {
268
+ "name": "Costa Rica",
269
+ "lat": 10,
270
+ "lng": -84
271
+ },
272
+ "CU": {
273
+ "name": "Cuba",
274
+ "lat": 21.5,
275
+ "lng": -80
276
+ },
277
+ "CW": {
278
+ "name": "Curaçao",
279
+ "lat": 12.116667,
280
+ "lng": -68.933333
281
+ },
282
+ "CX": {
283
+ "name": "Christmas Island",
284
+ "lat": -10.5,
285
+ "lng": 105.66666666
286
+ },
287
+ "KY": {
288
+ "name": "Cayman Islands",
289
+ "lat": 19.5,
290
+ "lng": -80.5
291
+ },
292
+ "CY": {
293
+ "name": "Cyprus",
294
+ "lat": 35,
295
+ "lng": 33
296
+ },
297
+ "CZ": {
298
+ "name": "Czechia",
299
+ "lat": 49.75,
300
+ "lng": 15.5
301
+ },
302
+ "DE": {
303
+ "name": "Germany",
304
+ "lat": 51,
305
+ "lng": 9
306
+ },
307
+ "DJ": {
308
+ "name": "Djibouti",
309
+ "lat": 11.5,
310
+ "lng": 43
311
+ },
312
+ "DM": {
313
+ "name": "Dominica",
314
+ "lat": 15.41666666,
315
+ "lng": -61.33333333
316
+ },
317
+ "DK": {
318
+ "name": "Denmark",
319
+ "lat": 56,
320
+ "lng": 10
321
+ },
322
+ "DO": {
323
+ "name": "Dominican Republic",
324
+ "lat": 19,
325
+ "lng": -70.66666666
326
+ },
327
+ "DZ": {
328
+ "name": "Algeria",
329
+ "lat": 28,
330
+ "lng": 3
331
+ },
332
+ "EC": {
333
+ "name": "Ecuador",
334
+ "lat": -2,
335
+ "lng": -77.5
336
+ },
337
+ "EG": {
338
+ "name": "Egypt",
339
+ "lat": 27,
340
+ "lng": 30
341
+ },
342
+ "ER": {
343
+ "name": "Eritrea",
344
+ "lat": 15,
345
+ "lng": 39
346
+ },
347
+ "EH": {
348
+ "name": "Western Sahara",
349
+ "lat": 24.5,
350
+ "lng": -13
351
+ },
352
+ "ES": {
353
+ "name": "Spain",
354
+ "lat": 40,
355
+ "lng": -4
356
+ },
357
+ "EE": {
358
+ "name": "Estonia",
359
+ "lat": 59,
360
+ "lng": 26
361
+ },
362
+ "ET": {
363
+ "name": "Ethiopia",
364
+ "lat": 8,
365
+ "lng": 38
366
+ },
367
+ "FI": {
368
+ "name": "Finland",
369
+ "lat": 64,
370
+ "lng": 26
371
+ },
372
+ "FJ": {
373
+ "name": "Fiji",
374
+ "lat": -18,
375
+ "lng": 175
376
+ },
377
+ "FK": {
378
+ "name": "Falkland Islands",
379
+ "lat": -51.75,
380
+ "lng": -59
381
+ },
382
+ "FR": {
383
+ "name": "France",
384
+ "lat": 46,
385
+ "lng": 2
386
+ },
387
+ "FO": {
388
+ "name": "Faroe Islands",
389
+ "lat": 62,
390
+ "lng": -7
391
+ },
392
+ "FM": {
393
+ "name": "Micronesia",
394
+ "lat": 6.91666666,
395
+ "lng": 158.25
396
+ },
397
+ "GA": {
398
+ "name": "Gabon",
399
+ "lat": -1,
400
+ "lng": 11.75
401
+ },
402
+ "GB": {
403
+ "name": "United Kingdom",
404
+ "lat": 54,
405
+ "lng": -2
406
+ },
407
+ "GE": {
408
+ "name": "Georgia",
409
+ "lat": 42,
410
+ "lng": 43.5
411
+ },
412
+ "GG": {
413
+ "name": "Guernsey",
414
+ "lat": 49.46666666,
415
+ "lng": -2.58333333
416
+ },
417
+ "GH": {
418
+ "name": "Ghana",
419
+ "lat": 8,
420
+ "lng": -2
421
+ },
422
+ "GI": {
423
+ "name": "Gibraltar",
424
+ "lat": 36.13333333,
425
+ "lng": -5.35
426
+ },
427
+ "GN": {
428
+ "name": "Guinea",
429
+ "lat": 11,
430
+ "lng": -10
431
+ },
432
+ "GP": {
433
+ "name": "Guadeloupe",
434
+ "lat": 16.25,
435
+ "lng": -61.583333
436
+ },
437
+ "GM": {
438
+ "name": "Gambia",
439
+ "lat": 13.46666666,
440
+ "lng": -16.56666666
441
+ },
442
+ "GW": {
443
+ "name": "Guinea-Bissau",
444
+ "lat": 12,
445
+ "lng": -15
446
+ },
447
+ "GQ": {
448
+ "name": "Equatorial Guinea",
449
+ "lat": 2,
450
+ "lng": 10
451
+ },
452
+ "GR": {
453
+ "name": "Greece",
454
+ "lat": 39,
455
+ "lng": 22
456
+ },
457
+ "GD": {
458
+ "name": "Grenada",
459
+ "lat": 12.11666666,
460
+ "lng": -61.66666666
461
+ },
462
+ "GL": {
463
+ "name": "Greenland",
464
+ "lat": 72,
465
+ "lng": -40
466
+ },
467
+ "GT": {
468
+ "name": "Guatemala",
469
+ "lat": 15.5,
470
+ "lng": -90.25
471
+ },
472
+ "GF": {
473
+ "name": "French Guiana",
474
+ "lat": 4,
475
+ "lng": -53
476
+ },
477
+ "GU": {
478
+ "name": "Guam",
479
+ "lat": 13.46666666,
480
+ "lng": 144.78333333
481
+ },
482
+ "GY": {
483
+ "name": "Guyana",
484
+ "lat": 5,
485
+ "lng": -59
486
+ },
487
+ "HK": {
488
+ "name": "Hong Kong",
489
+ "lat": 22.267,
490
+ "lng": 114.188
491
+ },
492
+ "HM": {
493
+ "name": "Heard Island and McDonald Islands",
494
+ "lat": -53.1,
495
+ "lng": 72.51666666
496
+ },
497
+ "HN": {
498
+ "name": "Honduras",
499
+ "lat": 15,
500
+ "lng": -86.5
501
+ },
502
+ "HR": {
503
+ "name": "Croatia",
504
+ "lat": 45.16666666,
505
+ "lng": 15.5
506
+ },
507
+ "HT": {
508
+ "name": "Haiti",
509
+ "lat": 19,
510
+ "lng": -72.41666666
511
+ },
512
+ "HU": {
513
+ "name": "Hungary",
514
+ "lat": 47,
515
+ "lng": 20
516
+ },
517
+ "ID": {
518
+ "name": "Indonesia",
519
+ "lat": -5,
520
+ "lng": 120
521
+ },
522
+ "IM": {
523
+ "name": "Isle of Man",
524
+ "lat": 54.25,
525
+ "lng": -4.5
526
+ },
527
+ "IN": {
528
+ "name": "India",
529
+ "lat": 20,
530
+ "lng": 77
531
+ },
532
+ "IO": {
533
+ "name": "British Indian Ocean Territory",
534
+ "lat": -6,
535
+ "lng": 71.5
536
+ },
537
+ "IE": {
538
+ "name": "Ireland",
539
+ "lat": 53,
540
+ "lng": -8
541
+ },
542
+ "IR": {
543
+ "name": "Iran",
544
+ "lat": 32,
545
+ "lng": 53
546
+ },
547
+ "IQ": {
548
+ "name": "Iraq",
549
+ "lat": 33,
550
+ "lng": 44
551
+ },
552
+ "IS": {
553
+ "name": "Iceland",
554
+ "lat": 65,
555
+ "lng": -18
556
+ },
557
+ "IL": {
558
+ "name": "Israel",
559
+ "lat": 31.47,
560
+ "lng": 35.13
561
+ },
562
+ "IT": {
563
+ "name": "Italy",
564
+ "lat": 42.83333333,
565
+ "lng": 12.83333333
566
+ },
567
+ "JM": {
568
+ "name": "Jamaica",
569
+ "lat": 18.25,
570
+ "lng": -77.5
571
+ },
572
+ "JE": {
573
+ "name": "Jersey",
574
+ "lat": 49.25,
575
+ "lng": -2.16666666
576
+ },
577
+ "JO": {
578
+ "name": "Jordan",
579
+ "lat": 31,
580
+ "lng": 36
581
+ },
582
+ "JP": {
583
+ "name": "Japan",
584
+ "lat": 36,
585
+ "lng": 138
586
+ },
587
+ "KZ": {
588
+ "name": "Kazakhstan",
589
+ "lat": 48,
590
+ "lng": 68
591
+ },
592
+ "KE": {
593
+ "name": "Kenya",
594
+ "lat": 1,
595
+ "lng": 38
596
+ },
597
+ "KG": {
598
+ "name": "Kyrgyzstan",
599
+ "lat": 41,
600
+ "lng": 75
601
+ },
602
+ "KH": {
603
+ "name": "Cambodia",
604
+ "lat": 13,
605
+ "lng": 105
606
+ },
607
+ "KI": {
608
+ "name": "Kiribati",
609
+ "lat": 1.41666666,
610
+ "lng": 173
611
+ },
612
+ "KN": {
613
+ "name": "Saint Kitts and Nevis",
614
+ "lat": 17.33333333,
615
+ "lng": -62.75
616
+ },
617
+ "KR": {
618
+ "name": "South Korea",
619
+ "lat": 37,
620
+ "lng": 127.5
621
+ },
622
+ "XK": {
623
+ "name": "Kosovo",
624
+ "lat": 42.666667,
625
+ "lng": 21.166667
626
+ },
627
+ "KW": {
628
+ "name": "Kuwait",
629
+ "lat": 29.5,
630
+ "lng": 45.75
631
+ },
632
+ "LA": {
633
+ "name": "Laos",
634
+ "lat": 18,
635
+ "lng": 105
636
+ },
637
+ "LB": {
638
+ "name": "Lebanon",
639
+ "lat": 33.83333333,
640
+ "lng": 35.83333333
641
+ },
642
+ "LR": {
643
+ "name": "Liberia",
644
+ "lat": 6.5,
645
+ "lng": -9.5
646
+ },
647
+ "LY": {
648
+ "name": "Libya",
649
+ "lat": 25,
650
+ "lng": 17
651
+ },
652
+ "LC": {
653
+ "name": "Saint Lucia",
654
+ "lat": 13.88333333,
655
+ "lng": -60.96666666
656
+ },
657
+ "LI": {
658
+ "name": "Liechtenstein",
659
+ "lat": 47.26666666,
660
+ "lng": 9.53333333
661
+ },
662
+ "LK": {
663
+ "name": "Sri Lanka",
664
+ "lat": 7,
665
+ "lng": 81
666
+ },
667
+ "LS": {
668
+ "name": "Lesotho",
669
+ "lat": -29.5,
670
+ "lng": 28.5
671
+ },
672
+ "LT": {
673
+ "name": "Lithuania",
674
+ "lat": 56,
675
+ "lng": 24
676
+ },
677
+ "LU": {
678
+ "name": "Luxembourg",
679
+ "lat": 49.75,
680
+ "lng": 6.16666666
681
+ },
682
+ "LV": {
683
+ "name": "Latvia",
684
+ "lat": 57,
685
+ "lng": 25
686
+ },
687
+ "MO": {
688
+ "name": "Macau",
689
+ "lat": 22.16666666,
690
+ "lng": 113.55
691
+ },
692
+ "MF": {
693
+ "name": "Saint Martin",
694
+ "lat": 18.08333333,
695
+ "lng": -63.95
696
+ },
697
+ "MA": {
698
+ "name": "Morocco",
699
+ "lat": 32,
700
+ "lng": -5
701
+ },
702
+ "MC": {
703
+ "name": "Monaco",
704
+ "lat": 43.73333333,
705
+ "lng": 7.4
706
+ },
707
+ "MD": {
708
+ "name": "Moldova",
709
+ "lat": 47,
710
+ "lng": 29
711
+ },
712
+ "MG": {
713
+ "name": "Madagascar",
714
+ "lat": -20,
715
+ "lng": 47
716
+ },
717
+ "MV": {
718
+ "name": "Maldives",
719
+ "lat": 3.25,
720
+ "lng": 73
721
+ },
722
+ "MX": {
723
+ "name": "Mexico",
724
+ "lat": 23,
725
+ "lng": -102
726
+ },
727
+ "MH": {
728
+ "name": "Marshall Islands",
729
+ "lat": 9,
730
+ "lng": 168
731
+ },
732
+ "MK": {
733
+ "name": "North Macedonia",
734
+ "lat": 41.83333333,
735
+ "lng": 22
736
+ },
737
+ "ML": {
738
+ "name": "Mali",
739
+ "lat": 17,
740
+ "lng": -4
741
+ },
742
+ "MT": {
743
+ "name": "Malta",
744
+ "lat": 35.83333333,
745
+ "lng": 14.58333333
746
+ },
747
+ "MM": {
748
+ "name": "Myanmar",
749
+ "lat": 22,
750
+ "lng": 98
751
+ },
752
+ "ME": {
753
+ "name": "Montenegro",
754
+ "lat": 42.5,
755
+ "lng": 19.3
756
+ },
757
+ "MN": {
758
+ "name": "Mongolia",
759
+ "lat": 46,
760
+ "lng": 105
761
+ },
762
+ "MP": {
763
+ "name": "Northern Mariana Islands",
764
+ "lat": 15.2,
765
+ "lng": 145.75
766
+ },
767
+ "MZ": {
768
+ "name": "Mozambique",
769
+ "lat": -18.25,
770
+ "lng": 35
771
+ },
772
+ "MR": {
773
+ "name": "Mauritania",
774
+ "lat": 20,
775
+ "lng": -12
776
+ },
777
+ "MS": {
778
+ "name": "Montserrat",
779
+ "lat": 16.75,
780
+ "lng": -62.2
781
+ },
782
+ "MQ": {
783
+ "name": "Martinique",
784
+ "lat": 14.666667,
785
+ "lng": -61
786
+ },
787
+ "MU": {
788
+ "name": "Mauritius",
789
+ "lat": -20.28333333,
790
+ "lng": 57.55
791
+ },
792
+ "MW": {
793
+ "name": "Malawi",
794
+ "lat": -13.5,
795
+ "lng": 34
796
+ },
797
+ "MY": {
798
+ "name": "Malaysia",
799
+ "lat": 2.5,
800
+ "lng": 112.5
801
+ },
802
+ "YT": {
803
+ "name": "Mayotte",
804
+ "lat": -12.83333333,
805
+ "lng": 45.16666666
806
+ },
807
+ "NA": {
808
+ "name": "Namibia",
809
+ "lat": -22,
810
+ "lng": 17
811
+ },
812
+ "NC": {
813
+ "name": "New Caledonia",
814
+ "lat": -21.5,
815
+ "lng": 165.5
816
+ },
817
+ "NE": {
818
+ "name": "Niger",
819
+ "lat": 16,
820
+ "lng": 8
821
+ },
822
+ "NF": {
823
+ "name": "Norfolk Island",
824
+ "lat": -29.03333333,
825
+ "lng": 167.95
826
+ },
827
+ "NG": {
828
+ "name": "Nigeria",
829
+ "lat": 10,
830
+ "lng": 8
831
+ },
832
+ "NI": {
833
+ "name": "Nicaragua",
834
+ "lat": 13,
835
+ "lng": -85
836
+ },
837
+ "NU": {
838
+ "name": "Niue",
839
+ "lat": -19.03333333,
840
+ "lng": -169.86666666
841
+ },
842
+ "NL": {
843
+ "name": "Netherlands",
844
+ "lat": 52.5,
845
+ "lng": 5.75
846
+ },
847
+ "NO": {
848
+ "name": "Norway",
849
+ "lat": 62,
850
+ "lng": 10
851
+ },
852
+ "NP": {
853
+ "name": "Nepal",
854
+ "lat": 28,
855
+ "lng": 84
856
+ },
857
+ "NR": {
858
+ "name": "Nauru",
859
+ "lat": -0.53333333,
860
+ "lng": 166.91666666
861
+ },
862
+ "NZ": {
863
+ "name": "New Zealand",
864
+ "lat": -41,
865
+ "lng": 174
866
+ },
867
+ "OM": {
868
+ "name": "Oman",
869
+ "lat": 21,
870
+ "lng": 57
871
+ },
872
+ "PK": {
873
+ "name": "Pakistan",
874
+ "lat": 30,
875
+ "lng": 70
876
+ },
877
+ "PA": {
878
+ "name": "Panama",
879
+ "lat": 9,
880
+ "lng": -80
881
+ },
882
+ "PN": {
883
+ "name": "Pitcairn Islands",
884
+ "lat": -25.06666666,
885
+ "lng": -130.1
886
+ },
887
+ "PE": {
888
+ "name": "Peru",
889
+ "lat": -10,
890
+ "lng": -76
891
+ },
892
+ "PH": {
893
+ "name": "Philippines",
894
+ "lat": 13,
895
+ "lng": 122
896
+ },
897
+ "PW": {
898
+ "name": "Palau",
899
+ "lat": 7.5,
900
+ "lng": 134.5
901
+ },
902
+ "PG": {
903
+ "name": "Papua New Guinea",
904
+ "lat": -6,
905
+ "lng": 147
906
+ },
907
+ "PL": {
908
+ "name": "Poland",
909
+ "lat": 52,
910
+ "lng": 20
911
+ },
912
+ "PR": {
913
+ "name": "Puerto Rico",
914
+ "lat": 18.25,
915
+ "lng": -66.5
916
+ },
917
+ "KP": {
918
+ "name": "North Korea",
919
+ "lat": 40,
920
+ "lng": 127
921
+ },
922
+ "PT": {
923
+ "name": "Portugal",
924
+ "lat": 39.5,
925
+ "lng": -8
926
+ },
927
+ "PY": {
928
+ "name": "Paraguay",
929
+ "lat": -23,
930
+ "lng": -58
931
+ },
932
+ "PS": {
933
+ "name": "Palestine",
934
+ "lat": 31.9,
935
+ "lng": 35.2
936
+ },
937
+ "PF": {
938
+ "name": "French Polynesia",
939
+ "lat": -15,
940
+ "lng": -140
941
+ },
942
+ "QA": {
943
+ "name": "Qatar",
944
+ "lat": 25.5,
945
+ "lng": 51.25
946
+ },
947
+ "RE": {
948
+ "name": "Réunion",
949
+ "lat": -21.15,
950
+ "lng": 55.5
951
+ },
952
+ "RO": {
953
+ "name": "Romania",
954
+ "lat": 46,
955
+ "lng": 25
956
+ },
957
+ "RU": {
958
+ "name": "Russia",
959
+ "lat": 60,
960
+ "lng": 100
961
+ },
962
+ "RW": {
963
+ "name": "Rwanda",
964
+ "lat": -2,
965
+ "lng": 30
966
+ },
967
+ "SA": {
968
+ "name": "Saudi Arabia",
969
+ "lat": 25,
970
+ "lng": 45
971
+ },
972
+ "SD": {
973
+ "name": "Sudan",
974
+ "lat": 15,
975
+ "lng": 30
976
+ },
977
+ "SN": {
978
+ "name": "Senegal",
979
+ "lat": 14,
980
+ "lng": -14
981
+ },
982
+ "SG": {
983
+ "name": "Singapore",
984
+ "lat": 1.36666666,
985
+ "lng": 103.8
986
+ },
987
+ "GS": {
988
+ "name": "South Georgia",
989
+ "lat": -54.5,
990
+ "lng": -37
991
+ },
992
+ "SJ": {
993
+ "name": "Svalbard and Jan Mayen",
994
+ "lat": 78,
995
+ "lng": 20
996
+ },
997
+ "SB": {
998
+ "name": "Solomon Islands",
999
+ "lat": -8,
1000
+ "lng": 159
1001
+ },
1002
+ "SL": {
1003
+ "name": "Sierra Leone",
1004
+ "lat": 8.5,
1005
+ "lng": -11.5
1006
+ },
1007
+ "SV": {
1008
+ "name": "El Salvador",
1009
+ "lat": 13.83333333,
1010
+ "lng": -88.91666666
1011
+ },
1012
+ "SM": {
1013
+ "name": "San Marino",
1014
+ "lat": 43.76666666,
1015
+ "lng": 12.41666666
1016
+ },
1017
+ "SO": {
1018
+ "name": "Somalia",
1019
+ "lat": 10,
1020
+ "lng": 49
1021
+ },
1022
+ "PM": {
1023
+ "name": "Saint Pierre and Miquelon",
1024
+ "lat": 46.83333333,
1025
+ "lng": -56.33333333
1026
+ },
1027
+ "RS": {
1028
+ "name": "Serbia",
1029
+ "lat": 44,
1030
+ "lng": 21
1031
+ },
1032
+ "SS": {
1033
+ "name": "South Sudan",
1034
+ "lat": 7,
1035
+ "lng": 30
1036
+ },
1037
+ "ST": {
1038
+ "name": "São Tomé and Príncipe",
1039
+ "lat": 1,
1040
+ "lng": 7
1041
+ },
1042
+ "SR": {
1043
+ "name": "Suriname",
1044
+ "lat": 4,
1045
+ "lng": -56
1046
+ },
1047
+ "SK": {
1048
+ "name": "Slovakia",
1049
+ "lat": 48.66666666,
1050
+ "lng": 19.5
1051
+ },
1052
+ "SI": {
1053
+ "name": "Slovenia",
1054
+ "lat": 46.11666666,
1055
+ "lng": 14.81666666
1056
+ },
1057
+ "SE": {
1058
+ "name": "Sweden",
1059
+ "lat": 62,
1060
+ "lng": 15
1061
+ },
1062
+ "SZ": {
1063
+ "name": "Eswatini",
1064
+ "lat": -26.5,
1065
+ "lng": 31.5
1066
+ },
1067
+ "SX": {
1068
+ "name": "Sint Maarten",
1069
+ "lat": 18.033333,
1070
+ "lng": -63.05
1071
+ },
1072
+ "SC": {
1073
+ "name": "Seychelles",
1074
+ "lat": -4.58333333,
1075
+ "lng": 55.66666666
1076
+ },
1077
+ "SY": {
1078
+ "name": "Syria",
1079
+ "lat": 35,
1080
+ "lng": 38
1081
+ },
1082
+ "TC": {
1083
+ "name": "Turks and Caicos Islands",
1084
+ "lat": 21.75,
1085
+ "lng": -71.58333333
1086
+ },
1087
+ "TD": {
1088
+ "name": "Chad",
1089
+ "lat": 15,
1090
+ "lng": 19
1091
+ },
1092
+ "TG": {
1093
+ "name": "Togo",
1094
+ "lat": 8,
1095
+ "lng": 1.16666666
1096
+ },
1097
+ "TH": {
1098
+ "name": "Thailand",
1099
+ "lat": 15,
1100
+ "lng": 100
1101
+ },
1102
+ "TJ": {
1103
+ "name": "Tajikistan",
1104
+ "lat": 39,
1105
+ "lng": 71
1106
+ },
1107
+ "TK": {
1108
+ "name": "Tokelau",
1109
+ "lat": -9,
1110
+ "lng": -172
1111
+ },
1112
+ "TM": {
1113
+ "name": "Turkmenistan",
1114
+ "lat": 40,
1115
+ "lng": 60
1116
+ },
1117
+ "TL": {
1118
+ "name": "Timor-Leste",
1119
+ "lat": -8.83333333,
1120
+ "lng": 125.91666666
1121
+ },
1122
+ "TO": {
1123
+ "name": "Tonga",
1124
+ "lat": -20,
1125
+ "lng": -175
1126
+ },
1127
+ "TT": {
1128
+ "name": "Trinidad and Tobago",
1129
+ "lat": 11,
1130
+ "lng": -61
1131
+ },
1132
+ "TN": {
1133
+ "name": "Tunisia",
1134
+ "lat": 34,
1135
+ "lng": 9
1136
+ },
1137
+ "TR": {
1138
+ "name": "Türkiye",
1139
+ "lat": 39,
1140
+ "lng": 35
1141
+ },
1142
+ "TV": {
1143
+ "name": "Tuvalu",
1144
+ "lat": -8,
1145
+ "lng": 178
1146
+ },
1147
+ "TW": {
1148
+ "name": "Taiwan",
1149
+ "lat": 23.5,
1150
+ "lng": 121
1151
+ },
1152
+ "TZ": {
1153
+ "name": "Tanzania",
1154
+ "lat": -6,
1155
+ "lng": 35
1156
+ },
1157
+ "UG": {
1158
+ "name": "Uganda",
1159
+ "lat": 1,
1160
+ "lng": 32
1161
+ },
1162
+ "UA": {
1163
+ "name": "Ukraine",
1164
+ "lat": 49,
1165
+ "lng": 32
1166
+ },
1167
+ "UM": {
1168
+ "name": "United States Minor Outlying Islands",
1169
+ "lat": 19.3,
1170
+ "lng": 166.633333
1171
+ },
1172
+ "UY": {
1173
+ "name": "Uruguay",
1174
+ "lat": -33,
1175
+ "lng": -56
1176
+ },
1177
+ "US": {
1178
+ "name": "United States",
1179
+ "lat": 38,
1180
+ "lng": -97
1181
+ },
1182
+ "UZ": {
1183
+ "name": "Uzbekistan",
1184
+ "lat": 41,
1185
+ "lng": 64
1186
+ },
1187
+ "VA": {
1188
+ "name": "Vatican City",
1189
+ "lat": 41.9,
1190
+ "lng": 12.45
1191
+ },
1192
+ "VC": {
1193
+ "name": "Saint Vincent and the Grenadines",
1194
+ "lat": 13.25,
1195
+ "lng": -61.2
1196
+ },
1197
+ "VE": {
1198
+ "name": "Venezuela",
1199
+ "lat": 8,
1200
+ "lng": -66
1201
+ },
1202
+ "VG": {
1203
+ "name": "British Virgin Islands",
1204
+ "lat": 18.431383,
1205
+ "lng": -64.62305
1206
+ },
1207
+ "VI": {
1208
+ "name": "United States Virgin Islands",
1209
+ "lat": 18.35,
1210
+ "lng": -64.933333
1211
+ },
1212
+ "VN": {
1213
+ "name": "Vietnam",
1214
+ "lat": 16.16666666,
1215
+ "lng": 107.83333333
1216
+ },
1217
+ "VU": {
1218
+ "name": "Vanuatu",
1219
+ "lat": -16,
1220
+ "lng": 167
1221
+ },
1222
+ "WF": {
1223
+ "name": "Wallis and Futuna",
1224
+ "lat": -13.3,
1225
+ "lng": -176.2
1226
+ },
1227
+ "WS": {
1228
+ "name": "Samoa",
1229
+ "lat": -13.58333333,
1230
+ "lng": -172.33333333
1231
+ },
1232
+ "YE": {
1233
+ "name": "Yemen",
1234
+ "lat": 15,
1235
+ "lng": 48
1236
+ },
1237
+ "ZA": {
1238
+ "name": "South Africa",
1239
+ "lat": -29,
1240
+ "lng": 24
1241
+ },
1242
+ "ZM": {
1243
+ "name": "Zambia",
1244
+ "lat": -15,
1245
+ "lng": 30
1246
+ },
1247
+ "ZW": {
1248
+ "name": "Zimbabwe",
1249
+ "lat": -20,
1250
+ "lng": 30
1251
+ }
1252
+ }
assets/globe.css CHANGED
@@ -14,4 +14,19 @@
14
  height: 100%;
15
  min-height: calc(100vh - 6rem);
16
  background: #0d1117;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
 
14
  height: 100%;
15
  min-height: calc(100vh - 6rem);
16
  background: #0d1117;
17
+ border-radius: 12px;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .maplibregl-popup-content {
22
+ background: #111827;
23
+ color: #f9fafb;
24
+ border-radius: 8px;
25
+ padding: 8px 10px;
26
+ font-size: 13px;
27
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
28
+ }
29
+
30
+ .maplibregl-popup-tip {
31
+ border-top-color: #111827;
32
  }
assets/globe.js CHANGED
@@ -47,7 +47,7 @@ const GLOBE_DEFAULTS = {
47
  touchZoomRotate: true,
48
  interactive: true,
49
  showGeolocateControl: false,
50
- useCurrentLocation: true,
51
  geolocateZoom: 10,
52
  showUserLocation: true,
53
  };
@@ -114,6 +114,136 @@ function mergeGlobeOptions(...sources) {
114
  return merged;
115
  }
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  async function initGlobe(options = {}) {
118
  try {
119
  await ensureMapLibre();
@@ -157,6 +287,9 @@ async function initGlobe(options = {}) {
157
  ...mapOptions,
158
  });
159
 
 
 
 
160
  let userMarker = null;
161
 
162
  if (showGeolocateControl) {
@@ -203,9 +336,10 @@ async function initGlobe(options = {}) {
203
 
204
  map.on("load", () => {
205
  map.resize();
206
- if (useCurrentLocation) {
207
  flyToCurrentLocation();
208
  }
 
209
  });
210
 
211
  window.addEventListener("resize", () => map.resize());
 
47
  touchZoomRotate: true,
48
  interactive: true,
49
  showGeolocateControl: false,
50
+ useCurrentLocation: false,
51
  geolocateZoom: 10,
52
  showUserLocation: true,
53
  };
 
114
  return merged;
115
  }
116
 
117
+ const HIGHLIGHT_SOURCE = "borderless-highlights";
118
+ const HIGHLIGHT_LAYER = "borderless-highlight-circles";
119
+
120
+ window.BorderlessGlobe = {
121
+ map: null,
122
+ markers: [],
123
+ lastVersion: -1,
124
+ pendingState: null,
125
+ rootElement: null,
126
+
127
+ clearMarkers() {
128
+ for (const marker of this.markers) {
129
+ marker.remove();
130
+ }
131
+ this.markers = [];
132
+ },
133
+
134
+ clearHighlights() {
135
+ if (!this.map) return;
136
+ if (this.map.getLayer(HIGHLIGHT_LAYER)) {
137
+ this.map.removeLayer(HIGHLIGHT_LAYER);
138
+ }
139
+ if (this.map.getSource(HIGHLIGHT_SOURCE)) {
140
+ this.map.removeSource(HIGHLIGHT_SOURCE);
141
+ }
142
+ },
143
+
144
+ addMarkers(markers) {
145
+ if (!this.map || !markers?.length) return;
146
+ for (const marker of markers) {
147
+ const popup = marker.label
148
+ ? new maplibregl.Popup({ offset: 18, closeButton: false }).setText(marker.label)
149
+ : null;
150
+ const instance = new maplibregl.Marker({ color: "#f59e0b" })
151
+ .setLngLat([marker.lng, marker.lat])
152
+ .addTo(this.map);
153
+ if (popup) {
154
+ instance.setPopup(popup);
155
+ }
156
+ this.markers.push(instance);
157
+ }
158
+ },
159
+
160
+ addHighlights(highlights) {
161
+ if (!this.map || !highlights?.length) return;
162
+ const features = highlights.map((item) => ({
163
+ type: "Feature",
164
+ geometry: {
165
+ type: "Point",
166
+ coordinates: [item.lng, item.lat],
167
+ },
168
+ properties: {
169
+ iso2: item.iso2,
170
+ },
171
+ }));
172
+
173
+ this.map.addSource(HIGHLIGHT_SOURCE, {
174
+ type: "geojson",
175
+ data: {
176
+ type: "FeatureCollection",
177
+ features,
178
+ },
179
+ });
180
+
181
+ this.map.addLayer({
182
+ id: HIGHLIGHT_LAYER,
183
+ type: "circle",
184
+ source: HIGHLIGHT_SOURCE,
185
+ paint: {
186
+ "circle-radius": [
187
+ "interpolate",
188
+ ["linear"],
189
+ ["zoom"],
190
+ 1,
191
+ 18,
192
+ 4,
193
+ 28,
194
+ 8,
195
+ 42,
196
+ ],
197
+ "circle-color": "#f59e0b",
198
+ "circle-opacity": 0.28,
199
+ "circle-stroke-width": 2,
200
+ "circle-stroke-color": "#fbbf24",
201
+ "circle-stroke-opacity": 0.9,
202
+ },
203
+ });
204
+ },
205
+
206
+ applyFlyTo(flyTo) {
207
+ if (!this.map || !flyTo) return;
208
+ if (flyTo.bounds) {
209
+ this.map.fitBounds(flyTo.bounds, {
210
+ padding: flyTo.padding ?? 80,
211
+ duration: 1800,
212
+ essential: true,
213
+ });
214
+ return;
215
+ }
216
+ if (flyTo.center) {
217
+ this.map.flyTo({
218
+ center: flyTo.center,
219
+ zoom: flyTo.zoom ?? 4,
220
+ duration: 1800,
221
+ essential: true,
222
+ });
223
+ }
224
+ },
225
+
226
+ applyState(state) {
227
+ if (!state) return;
228
+ this.pendingState = state;
229
+ if (!this.map) return;
230
+ if (state.version === this.lastVersion) return;
231
+ this.lastVersion = state.version;
232
+
233
+ this.clearMarkers();
234
+ this.clearHighlights();
235
+ this.addMarkers(state.markers || []);
236
+ this.addHighlights(state.highlights || []);
237
+ this.applyFlyTo(state.fly_to);
238
+ },
239
+
240
+ onMapReady() {
241
+ if (this.pendingState) {
242
+ this.applyState(this.pendingState);
243
+ }
244
+ },
245
+ };
246
+
247
  async function initGlobe(options = {}) {
248
  try {
249
  await ensureMapLibre();
 
287
  ...mapOptions,
288
  });
289
 
290
+ window.BorderlessGlobe.map = map;
291
+ window.BorderlessGlobe.rootElement = element;
292
+
293
  let userMarker = null;
294
 
295
  if (showGeolocateControl) {
 
336
 
337
  map.on("load", () => {
338
  map.resize();
339
+ if (useCurrentLocation && !window.BorderlessGlobe.pendingState?.fly_to) {
340
  flyToCurrentLocation();
341
  }
342
+ window.BorderlessGlobe.onMapReady();
343
  });
344
 
345
  window.addEventListener("resize", () => map.resize());
ui/chat.py CHANGED
@@ -2,14 +2,18 @@
2
  import gradio as gr
3
 
4
  from ui.globe import render_globe_panel
 
5
  from ui.inference import respond
6
 
7
  ADDITIONAL_INPUTS = [
8
  gr.Textbox(
9
  value=(
10
- "You are a friendly chatbot with access to World Bank economic data "
11
- "and passport/visa information. Use tools when users ask about GDP, "
12
- "inflation, trade, visa requirements, or passport strength."
 
 
 
13
  ),
14
  label="System message",
15
  ),
@@ -54,7 +58,9 @@ def create_chat_interface(
54
  """
55
  For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
56
  """
57
- return SidebarChatInterface(
 
 
58
  respond,
59
  multimodal=True,
60
  textbox=gr.MultimodalTextbox(
@@ -64,5 +70,15 @@ def create_chat_interface(
64
  sources=["upload"],
65
  ),
66
  history_container=history_container,
67
- additional_inputs=ADDITIONAL_INPUTS,
 
 
 
 
 
 
 
 
 
68
  )
 
 
2
  import gradio as gr
3
 
4
  from ui.globe import render_globe_panel
5
+ from ui.globe_commands import empty_globe_state
6
  from ui.inference import respond
7
 
8
  ADDITIONAL_INPUTS = [
9
  gr.Textbox(
10
  value=(
11
+ "You are Borderless, an immigration research assistant with access to "
12
+ "World Bank economic data, passport/visa information, and an interactive "
13
+ "3D globe. Use data tools when users ask about GDP, inflation, trade, "
14
+ "visa requirements, or passport strength. When you recommend or compare "
15
+ "destination countries, call update_globe with action show so the user "
16
+ "can see them on the globe."
17
  ),
18
  label="System message",
19
  ),
 
58
  """
59
  For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
60
  """
61
+ globe_state = gr.JSON(value=empty_globe_state(), visible=False, elem_id="globe-state")
62
+
63
+ chat = SidebarChatInterface(
64
  respond,
65
  multimodal=True,
66
  textbox=gr.MultimodalTextbox(
 
70
  sources=["upload"],
71
  ),
72
  history_container=history_container,
73
+ additional_inputs=ADDITIONAL_INPUTS + [globe_state],
74
+ additional_outputs=[globe_state],
75
+ )
76
+
77
+ globe_state.change(
78
+ fn=None,
79
+ inputs=[globe_state],
80
+ outputs=[],
81
+ js="(state) => { window.BorderlessGlobe?.applyState(state); return []; }",
82
+ queue=False,
83
  )
84
+ return chat
ui/country_coords.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/country_coords.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+
8
+ ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
9
+
10
+
11
+ @lru_cache(maxsize=1)
12
+ def _load_centroids() -> dict[str, dict[str, float | str]]:
13
+ return json.loads((ASSETS_DIR / "country_centroids.json").read_text(encoding="utf-8"))
14
+
15
+
16
+ def lookup_country(iso2: str) -> dict[str, float | str] | None:
17
+ code = (iso2 or "").strip().upper()
18
+ if not code:
19
+ return None
20
+ return _load_centroids().get(code)
21
+
22
+
23
+ def resolve_countries(
24
+ countries: list[str],
25
+ labels: list[str] | None = None,
26
+ ) -> tuple[list[dict], list[str]]:
27
+ resolved: list[dict] = []
28
+ unknown: list[str] = []
29
+ label_list = labels or []
30
+
31
+ for index, raw_code in enumerate(countries):
32
+ code = (raw_code or "").strip().upper()
33
+ if not code:
34
+ continue
35
+ info = lookup_country(code)
36
+ if not info:
37
+ unknown.append(code)
38
+ continue
39
+ label = label_list[index] if index < len(label_list) else str(info["name"])
40
+ resolved.append(
41
+ {
42
+ "iso2": code,
43
+ "name": str(info["name"]),
44
+ "label": label,
45
+ "lat": float(info["lat"]),
46
+ "lng": float(info["lng"]),
47
+ }
48
+ )
49
+
50
+ return resolved, unknown
ui/globe.py CHANGED
@@ -37,7 +37,7 @@ def _map_data_attrs(
37
  touch_zoom_rotate: bool = True,
38
  interactive: bool = True,
39
  show_geolocate_control: bool = False,
40
- use_current_location: bool = True,
41
  geolocate_zoom: float = 10.0,
42
  track_user_location: bool = False,
43
  show_user_location: bool = True,
@@ -94,7 +94,7 @@ def render_globe_panel(
94
  touch_zoom_rotate: bool = True,
95
  interactive: bool = True,
96
  show_geolocate_control: bool = False,
97
- use_current_location: bool = True,
98
  geolocate_zoom: float = 10.0,
99
  track_user_location: bool = False,
100
  show_user_location: bool = True,
 
37
  touch_zoom_rotate: bool = True,
38
  interactive: bool = True,
39
  show_geolocate_control: bool = False,
40
+ use_current_location: bool = False,
41
  geolocate_zoom: float = 10.0,
42
  track_user_location: bool = False,
43
  show_user_location: bool = True,
 
94
  touch_zoom_rotate: bool = True,
95
  interactive: bool = True,
96
  show_geolocate_control: bool = False,
97
+ use_current_location: bool = False,
98
  geolocate_zoom: float = 10.0,
99
  track_user_location: bool = False,
100
  show_user_location: bool = True,
ui/globe_commands.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ui/globe_commands.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from ui.country_coords import resolve_countries
7
+
8
+
9
+ def empty_globe_state() -> dict[str, Any]:
10
+ return {"version": 0, "markers": [], "highlights": [], "fly_to": None}
11
+
12
+
13
+ def _next_version(state: dict[str, Any]) -> int:
14
+ return int(state.get("version", 0)) + 1
15
+
16
+
17
+ def _marker_payload(country: dict[str, Any]) -> dict[str, Any]:
18
+ return {
19
+ "iso2": country["iso2"],
20
+ "label": country["label"],
21
+ "lng": country["lng"],
22
+ "lat": country["lat"],
23
+ }
24
+
25
+
26
+ def _highlight_payload(country: dict[str, Any]) -> dict[str, Any]:
27
+ return {
28
+ "iso2": country["iso2"],
29
+ "lng": country["lng"],
30
+ "lat": country["lat"],
31
+ }
32
+
33
+
34
+ def _fly_to_payload(
35
+ countries: list[dict[str, Any]],
36
+ *,
37
+ zoom: float | None,
38
+ ) -> dict[str, Any] | None:
39
+ if not countries:
40
+ return None
41
+ if len(countries) == 1:
42
+ country = countries[0]
43
+ return {
44
+ "center": [country["lng"], country["lat"]],
45
+ "zoom": zoom if zoom is not None else 4.0,
46
+ }
47
+
48
+ lngs = [country["lng"] for country in countries]
49
+ lats = [country["lat"] for country in countries]
50
+ return {
51
+ "bounds": [[min(lngs), min(lats)], [max(lngs), max(lats)]],
52
+ "padding": 80,
53
+ }
54
+
55
+
56
+ def apply_update_globe(
57
+ state: dict[str, Any],
58
+ args: dict[str, Any],
59
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
60
+ action = (args.get("action") or "show").strip().lower()
61
+ countries = args.get("countries") or []
62
+ labels = args.get("labels")
63
+ zoom = args.get("zoom")
64
+
65
+ if action == "clear":
66
+ new_state = empty_globe_state()
67
+ new_state["version"] = _next_version(state)
68
+ return {"status": "cleared", "action": action}, new_state
69
+
70
+ if not countries:
71
+ return (
72
+ {"error": "countries is required unless action is clear"},
73
+ state,
74
+ )
75
+
76
+ resolved, unknown = resolve_countries(countries, labels)
77
+ if not resolved:
78
+ return (
79
+ {
80
+ "error": "No valid ISO-2 country codes were provided",
81
+ "unknown_codes": unknown,
82
+ },
83
+ state,
84
+ )
85
+
86
+ new_state = {
87
+ "version": _next_version(state),
88
+ "markers": list(state.get("markers", [])),
89
+ "highlights": list(state.get("highlights", [])),
90
+ "fly_to": state.get("fly_to"),
91
+ }
92
+
93
+ if action == "show":
94
+ new_state["markers"] = [_marker_payload(country) for country in resolved]
95
+ new_state["highlights"] = [_highlight_payload(country) for country in resolved]
96
+ new_state["fly_to"] = _fly_to_payload(resolved, zoom=zoom)
97
+ elif action in {"markers", "add_markers"}:
98
+ existing = {marker["iso2"] for marker in new_state["markers"]}
99
+ for country in resolved:
100
+ if country["iso2"] not in existing:
101
+ new_state["markers"].append(_marker_payload(country))
102
+ elif action == "highlight":
103
+ existing = {item["iso2"] for item in new_state["highlights"]}
104
+ for country in resolved:
105
+ if country["iso2"] not in existing:
106
+ new_state["highlights"].append(_highlight_payload(country))
107
+ elif action == "fly_to":
108
+ new_state["fly_to"] = _fly_to_payload(resolved, zoom=zoom)
109
+ else:
110
+ return (
111
+ {
112
+ "error": (
113
+ "Unknown action. Use show, markers, highlight, fly_to, or clear."
114
+ )
115
+ },
116
+ state,
117
+ )
118
+
119
+ result: dict[str, Any] = {
120
+ "status": "ok",
121
+ "action": action,
122
+ "countries": [
123
+ {
124
+ "iso2": country["iso2"],
125
+ "name": country["name"],
126
+ "label": country["label"],
127
+ }
128
+ for country in resolved
129
+ ],
130
+ }
131
+ if unknown:
132
+ result["unknown_codes"] = unknown
133
+ return result, new_state
ui/inference/respond.py CHANGED
@@ -21,6 +21,7 @@ def respond(
21
  max_tokens,
22
  temperature,
23
  top_p,
 
24
  hf_token: gr.OAuthToken,
25
  ):
26
  """
@@ -30,7 +31,11 @@ def respond(
30
 
31
  user_content = multimodal_input_to_api_content(message)
32
  if not user_content:
33
- yield from yield_response([], "Please enter a message or attach a file.")
 
 
 
 
34
  return
35
 
36
  api_messages: list[dict[str, Any]] = [
@@ -53,14 +58,19 @@ def respond(
53
  )
54
 
55
  if tool_calls:
56
- yield from execute_tool_calls(
57
- api_messages, ui_messages, tool_calls, content
58
- )
 
 
 
 
 
59
  continue
60
 
61
  answer = content or reasoning
62
  if answer:
63
- yield from yield_response(ui_messages, answer)
64
  return
65
 
66
  # Retry once with a required tool call when the model returned nothing.
@@ -74,14 +84,19 @@ def respond(
74
  tool_choice="required",
75
  )
76
  if tool_calls:
77
- yield from execute_tool_calls(
78
- api_messages, ui_messages, tool_calls, content
79
- )
 
 
 
 
 
80
  continue
81
 
82
  answer = content or reasoning
83
  if answer:
84
- yield from yield_response(ui_messages, answer)
85
  return
86
 
87
  # Some providers return an empty first turn when tools are enabled.
@@ -95,12 +110,13 @@ def respond(
95
  )
96
  answer = content or reasoning
97
  if answer:
98
- yield from yield_response(ui_messages, answer)
99
  return
100
 
101
  yield from yield_response(
102
  ui_messages,
103
  "I could not generate a response. Please try again.",
 
104
  )
105
  return
106
 
@@ -108,10 +124,12 @@ def respond(
108
  yield from yield_response(
109
  ui_messages,
110
  f"Sorry, something went wrong while generating a response: {exc}",
 
111
  )
112
  return
113
 
114
  yield from yield_response(
115
  ui_messages,
116
  "I reached the maximum number of tool calls for this request.",
 
117
  )
 
21
  max_tokens,
22
  temperature,
23
  top_p,
24
+ globe_state: dict[str, Any],
25
  hf_token: gr.OAuthToken,
26
  ):
27
  """
 
31
 
32
  user_content = multimodal_input_to_api_content(message)
33
  if not user_content:
34
+ yield from yield_response(
35
+ [],
36
+ "Please enter a message or attach a file.",
37
+ globe_state,
38
+ )
39
  return
40
 
41
  api_messages: list[dict[str, Any]] = [
 
58
  )
59
 
60
  if tool_calls:
61
+ for ui_msg, globe_state in execute_tool_calls(
62
+ api_messages,
63
+ ui_messages,
64
+ tool_calls,
65
+ content,
66
+ globe_state,
67
+ ):
68
+ yield ui_msg, globe_state
69
  continue
70
 
71
  answer = content or reasoning
72
  if answer:
73
+ yield from yield_response(ui_messages, answer, globe_state)
74
  return
75
 
76
  # Retry once with a required tool call when the model returned nothing.
 
84
  tool_choice="required",
85
  )
86
  if tool_calls:
87
+ for ui_msg, globe_state in execute_tool_calls(
88
+ api_messages,
89
+ ui_messages,
90
+ tool_calls,
91
+ content,
92
+ globe_state,
93
+ ):
94
+ yield ui_msg, globe_state
95
  continue
96
 
97
  answer = content or reasoning
98
  if answer:
99
+ yield from yield_response(ui_messages, answer, globe_state)
100
  return
101
 
102
  # Some providers return an empty first turn when tools are enabled.
 
110
  )
111
  answer = content or reasoning
112
  if answer:
113
+ yield from yield_response(ui_messages, answer, globe_state)
114
  return
115
 
116
  yield from yield_response(
117
  ui_messages,
118
  "I could not generate a response. Please try again.",
119
+ globe_state,
120
  )
121
  return
122
 
 
124
  yield from yield_response(
125
  ui_messages,
126
  f"Sorry, something went wrong while generating a response: {exc}",
127
+ globe_state,
128
  )
129
  return
130
 
131
  yield from yield_response(
132
  ui_messages,
133
  "I reached the maximum number of tool calls for this request.",
134
+ globe_state,
135
  )
ui/inference/streaming.py CHANGED
@@ -1,32 +1,42 @@
1
  # ui/inference/streaming.py
2
  from __future__ import annotations
3
 
 
 
4
  from gradio import ChatMessage
5
 
6
 
7
- def yield_streaming_string(text: str):
8
  if not text:
9
- yield ""
10
  return
11
  for index in range(len(text)):
12
- yield text[: index + 1]
13
 
14
 
15
- def yield_streaming_messages(ui_messages: list[ChatMessage], text: str):
 
 
 
 
16
  messages = list(ui_messages)
17
  if not text:
18
  messages.append(ChatMessage(role="assistant", content=""))
19
- yield messages
20
  return
21
 
22
  messages.append(ChatMessage(role="assistant", content=""))
23
  for index in range(len(text)):
24
  messages[-1] = ChatMessage(role="assistant", content=text[: index + 1])
25
- yield messages
26
 
27
 
28
- def yield_response(ui_messages: list[ChatMessage], text: str):
 
 
 
 
29
  if ui_messages:
30
- yield from yield_streaming_messages(ui_messages, text)
31
  else:
32
- yield from yield_streaming_string(text)
 
1
  # ui/inference/streaming.py
2
  from __future__ import annotations
3
 
4
+ from typing import Any
5
+
6
  from gradio import ChatMessage
7
 
8
 
9
+ def yield_streaming_string(text: str, globe_state: dict[str, Any]):
10
  if not text:
11
+ yield "", globe_state
12
  return
13
  for index in range(len(text)):
14
+ yield text[: index + 1], globe_state
15
 
16
 
17
+ def yield_streaming_messages(
18
+ ui_messages: list[ChatMessage],
19
+ text: str,
20
+ globe_state: dict[str, Any],
21
+ ):
22
  messages = list(ui_messages)
23
  if not text:
24
  messages.append(ChatMessage(role="assistant", content=""))
25
+ yield messages, globe_state
26
  return
27
 
28
  messages.append(ChatMessage(role="assistant", content=""))
29
  for index in range(len(text)):
30
  messages[-1] = ChatMessage(role="assistant", content=text[: index + 1])
31
+ yield messages, globe_state
32
 
33
 
34
+ def yield_response(
35
+ ui_messages: list[ChatMessage],
36
+ text: str,
37
+ globe_state: dict[str, Any],
38
+ ):
39
  if ui_messages:
40
+ yield from yield_streaming_messages(ui_messages, text, globe_state)
41
  else:
42
+ yield from yield_streaming_string(text, globe_state)
ui/inference/tool_schemas/__init__.py CHANGED
@@ -5,9 +5,11 @@ from typing import Any
5
  from .get_economic_indicator import SCHEMA as get_economic_indicator
6
  from .get_passport_stats import SCHEMA as get_passport_stats
7
  from .get_visa_requirement import SCHEMA as get_visa_requirement
 
8
 
9
  TOOL_SCHEMAS: list[dict[str, Any]] = [
10
  get_economic_indicator,
11
  get_visa_requirement,
12
  get_passport_stats,
 
13
  ]
 
5
  from .get_economic_indicator import SCHEMA as get_economic_indicator
6
  from .get_passport_stats import SCHEMA as get_passport_stats
7
  from .get_visa_requirement import SCHEMA as get_visa_requirement
8
+ from .update_globe import SCHEMA as update_globe
9
 
10
  TOOL_SCHEMAS: list[dict[str, Any]] = [
11
  get_economic_indicator,
12
  get_visa_requirement,
13
  get_passport_stats,
14
+ update_globe,
15
  ]
ui/inference/tool_schemas/update_globe.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ SCHEMA: dict[str, Any] = {
6
+ "type": "function",
7
+ "function": {
8
+ "name": "update_globe",
9
+ "description": (
10
+ "Update the interactive 3D globe beside the chat. Use this when "
11
+ "presenting destination countries, comparing options, or guiding "
12
+ "the user geographically. Use ISO-2 country codes (e.g. US, CA, DE, IN, AU)."
13
+ ),
14
+ "parameters": {
15
+ "type": "object",
16
+ "properties": {
17
+ "action": {
18
+ "type": "string",
19
+ "enum": ["show", "markers", "highlight", "fly_to", "clear"],
20
+ "description": (
21
+ "show: replace globe overlays and fly to all countries "
22
+ "(best for a shortlist). markers: add pin markers. "
23
+ "highlight: add glowing highlights. fly_to: move the camera. "
24
+ "clear: remove markers and highlights."
25
+ ),
26
+ },
27
+ "countries": {
28
+ "type": "array",
29
+ "items": {"type": "string"},
30
+ "description": "ISO-2 country codes to show on the globe.",
31
+ },
32
+ "labels": {
33
+ "type": "array",
34
+ "items": {"type": "string"},
35
+ "description": (
36
+ "Optional labels for markers, parallel to countries "
37
+ "(e.g. 'Skilled worker visa')."
38
+ ),
39
+ },
40
+ "zoom": {
41
+ "type": "number",
42
+ "description": "Optional zoom level when flying to a single country.",
43
+ },
44
+ },
45
+ "required": ["action"],
46
+ },
47
+ },
48
+ }
ui/inference/tools.py CHANGED
@@ -9,6 +9,7 @@ from gradio import ChatMessage
9
 
10
  from apis.passport_visa import passport_stats, visa_requirement
11
  from apis.world_bank import get_indicator_series
 
12
 
13
  from .messages import assistant_message_dict
14
 
@@ -19,30 +20,50 @@ def truncate(text: str, limit: int = 4000) -> str:
19
  return text[:limit] + "\n… (truncated)"
20
 
21
 
22
- def run_tool(name: str, arguments: str) -> str:
 
 
 
 
 
23
  try:
24
  args = json.loads(arguments or "{}")
25
- result = _run_tool(name, args)
26
- return json.dumps(result, default=str)
27
  except Exception as exc:
28
- return json.dumps({"error": str(exc)})
29
 
30
 
31
- def _run_tool(name: str, args: dict[str, Any]) -> Any:
 
 
 
 
 
32
  if name == "get_economic_indicator":
33
- return get_indicator_series(
34
- indicator=args["indicator"],
35
- country=args["country"],
 
 
 
36
  )
37
  if name == "get_visa_requirement":
38
- return visa_requirement(
39
- passport_iso2=args["passport_iso2"],
40
- destination_iso2=args["destination_iso2"],
 
 
 
41
  )
42
  if name == "get_passport_stats":
43
- return passport_stats(passport_iso2=args["passport_iso2"])
 
 
 
 
44
 
45
- return {"error": f"Unknown tool: {name}"}
46
 
47
 
48
  def execute_tool_calls(
@@ -50,6 +71,7 @@ def execute_tool_calls(
50
  ui_messages: list[ChatMessage],
51
  tool_calls: list[dict[str, Any]],
52
  content: str,
 
53
  ):
54
  api_messages.append(assistant_message_dict(content, tool_calls))
55
 
@@ -71,9 +93,13 @@ def execute_tool_calls(
71
  },
72
  )
73
  )
74
- yield ui_messages
75
 
76
- result = run_tool(tool_name, tool_args)
 
 
 
 
77
  duration = time.monotonic() - started
78
 
79
  ui_messages[-1] = ChatMessage(
@@ -89,7 +115,7 @@ def execute_tool_calls(
89
  "duration": duration,
90
  },
91
  )
92
- yield ui_messages
93
 
94
  api_messages.append(
95
  {
 
9
 
10
  from apis.passport_visa import passport_stats, visa_requirement
11
  from apis.world_bank import get_indicator_series
12
+ from ui.globe_commands import apply_update_globe
13
 
14
  from .messages import assistant_message_dict
15
 
 
20
  return text[:limit] + "\n… (truncated)"
21
 
22
 
23
+ def run_tool(
24
+ name: str,
25
+ arguments: str,
26
+ *,
27
+ globe_state: dict[str, Any] | None = None,
28
+ ) -> tuple[str, dict[str, Any] | None]:
29
  try:
30
  args = json.loads(arguments or "{}")
31
+ result, updated_globe = _run_tool(name, args, globe_state=globe_state)
32
+ return json.dumps(result, default=str), updated_globe
33
  except Exception as exc:
34
+ return json.dumps({"error": str(exc)}), globe_state
35
 
36
 
37
+ def _run_tool(
38
+ name: str,
39
+ args: dict[str, Any],
40
+ *,
41
+ globe_state: dict[str, Any] | None = None,
42
+ ) -> tuple[Any, dict[str, Any] | None]:
43
  if name == "get_economic_indicator":
44
+ return (
45
+ get_indicator_series(
46
+ indicator=args["indicator"],
47
+ country=args["country"],
48
+ ),
49
+ globe_state,
50
  )
51
  if name == "get_visa_requirement":
52
+ return (
53
+ visa_requirement(
54
+ passport_iso2=args["passport_iso2"],
55
+ destination_iso2=args["destination_iso2"],
56
+ ),
57
+ globe_state,
58
  )
59
  if name == "get_passport_stats":
60
+ return passport_stats(passport_iso2=args["passport_iso2"]), globe_state
61
+ if name == "update_globe":
62
+ if globe_state is None:
63
+ return {"error": "Globe state is unavailable"}, None
64
+ return apply_update_globe(globe_state, args)
65
 
66
+ return {"error": f"Unknown tool: {name}"}, globe_state
67
 
68
 
69
  def execute_tool_calls(
 
71
  ui_messages: list[ChatMessage],
72
  tool_calls: list[dict[str, Any]],
73
  content: str,
74
+ globe_state: dict[str, Any],
75
  ):
76
  api_messages.append(assistant_message_dict(content, tool_calls))
77
 
 
93
  },
94
  )
95
  )
96
+ yield ui_messages, globe_state
97
 
98
+ result, globe_state = run_tool(
99
+ tool_name,
100
+ tool_args,
101
+ globe_state=globe_state,
102
+ )
103
  duration = time.monotonic() - started
104
 
105
  ui_messages[-1] = ChatMessage(
 
115
  "duration": duration,
116
  },
117
  )
118
+ yield ui_messages, globe_state
119
 
120
  api_messages.append(
121
  {