ginnyxxxxxxx commited on
Commit
529e548
Β·
1 Parent(s): 144e51b
Files changed (1) hide show
  1. app.py +143 -302
app.py CHANGED
@@ -4,14 +4,13 @@ import folium
4
  import numpy as np
5
  import os
6
  import re
7
- from huggingface_hub import InferenceClient
8
 
9
  BASE = os.path.dirname(os.path.abspath(__file__))
10
  STAY_POINTS = os.path.join(BASE, "data", "stay_points_sampled.csv")
11
  POI_PATH = os.path.join(BASE, "data", "poi_sampled.csv")
12
  DEMO_PATH = os.path.join(BASE, "data", "demographics_sampled.csv")
13
-
14
- MODEL_ID = "meta-llama/Llama-3.2-1B-Instruct"
15
 
16
  SEX_MAP = {1:"Male", 2:"Female", -8:"Unknown", -7:"Prefer not to answer"}
17
  EDU_MAP = {1:"Less than HS", 2:"HS Graduate/GED", 3:"Some College/Associate",
@@ -50,10 +49,60 @@ def parse_act_types(x):
50
  return str(x)
51
 
52
  sp["act_label"] = sp["act_types"].apply(parse_act_types)
 
 
 
 
 
 
 
 
 
 
53
  sample_agents = sorted(sp["agent_id"].unique().tolist())
54
  print(f"Ready. {len(sample_agents)} agents loaded.")
55
 
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  # ── Mobility text builders ────────────────────────────────────────────────────
58
 
59
  def build_mobility_summary(agent_sp):
@@ -90,7 +139,6 @@ def build_mobility_summary(agent_sp):
90
  return "night"
91
  agent_sp2["tod"] = agent_sp2["hour"].apply(tod)
92
  tod_pct = (agent_sp2["tod"].value_counts(normalize=True) * 100).round(0).astype(int)
93
-
94
  agent_sp2["is_weekend"] = agent_sp2["start_datetime"].dt.dayofweek >= 5
95
  wd_pct = int((~agent_sp2["is_weekend"]).mean() * 100)
96
 
@@ -121,302 +169,141 @@ def build_weekly_checkin(agent_sp):
121
  return "\n".join(lines)
122
 
123
 
124
- # ── Prompts ───────────────────────────────────────────────────────────────────
125
-
126
- STEP1_SYSTEM = """You are an expert mobility analyst. Extract objective features from the trajectory data.
127
- Respond with EXACTLY this structure, keep each point to one short sentence:
128
-
129
- LOCATION INVENTORY:
130
- - Top venues: [list top 3 with visit counts]
131
- - Price level: [budget/mid-range/high-end mix]
132
- - Neighborhood: [residential/commercial/urban/suburban]
133
-
134
- TEMPORAL PATTERNS:
135
- - Active hours: [time range]
136
- - Weekday/Weekend: [ratio]
137
- - Routine: [consistent/variable]
138
-
139
- SEQUENCE:
140
- - Typical chain: [e.g. Home β†’ Work β†’ Home]
141
- - Notable pattern: [one observation]
142
-
143
- Do NOT interpret or infer demographics. Be concise."""
144
-
145
- STEP2_SYSTEM = """You are an expert mobility analyst. Based on the extracted features, analyze behavioral patterns.
146
- Respond with EXACTLY this structure, one short sentence per point:
147
-
148
- SCHEDULE: [fixed/flexible/shift β€” one sentence]
149
- ECONOMIC: [budget/mid-range/premium spending β€” one sentence]
150
- SOCIAL: [family/individual/community focus β€” one sentence]
151
- LIFESTYLE: [urban professional/suburban/student/other β€” one sentence]
152
- STABILITY: [routine consistency β€” one sentence]
153
-
154
- Do NOT make income predictions yet. Be concise."""
155
-
156
- STEP3_SYSTEM = """You are an expert mobility analyst performing final income inference.
157
- Based on the trajectory features and behavioral analysis, output EXACTLY:
158
-
159
- INCOME_PREDICTION: [Very Low (<$15k) | Low ($15k-$35k) | Middle ($35k-$75k) | Upper-Middle ($75k-$125k) | High ($125k-$200k) | Very High (>$200k)]
160
- INCOME_CONFIDENCE: [1-5]
161
- INCOME_REASONING: [2-3 sentences linking specific mobility evidence to the prediction]
162
- ALTERNATIVES: [2nd most likely] | [3rd most likely]"""
163
-
164
-
165
- def call_llm(client, system_prompt, user_content, max_tokens=400):
166
- response = client.chat.completions.create(
167
- model=MODEL_ID,
168
- messages=[
169
- {"role": "system", "content": system_prompt},
170
- {"role": "user", "content": user_content},
171
- ],
172
- max_tokens=max_tokens,
173
- temperature=0.3,
174
- )
175
- return response.choices[0].message.content.strip()
176
-
177
-
178
- # ── HTML rendering ────────────────────────────────────────────────────────────
179
 
180
  CHAIN_CSS = """
181
  <style>
182
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap');
183
 
184
- .hicotraj-chain {
185
- font-family: 'IBM Plex Sans', sans-serif;
186
- padding: 12px 4px;
187
- max-width: 100%;
188
- }
189
 
190
- /* Stage cards */
191
  .stage-card {
192
- border-radius: 10px;
193
- padding: 16px 18px;
194
- margin-bottom: 0;
195
- position: relative;
196
- transition: box-shadow 0.3s;
197
  }
198
- .stage-card.dim { opacity: 0.35; filter: grayscale(0.4); }
199
- .stage-card.active { box-shadow: 0 4px 20px rgba(0,0,0,0.12); opacity: 1; filter: none; }
200
 
201
  .stage-card.s1 { background: #f8f9fc; border: 1.5px solid #c8d0e0; }
202
  .stage-card.s2 { background: #fdf6f0; border: 1.5px solid #e8c9a8; }
203
  .stage-card.s3 { background: #fff8f8; border: 2px solid #c0392b; }
204
 
205
- .stage-header {
206
- display: flex;
207
- align-items: center;
208
- gap: 10px;
209
- margin-bottom: 10px;
210
- }
211
  .stage-badge {
212
  font-family: 'IBM Plex Mono', monospace;
213
- font-size: 10px;
214
- font-weight: 600;
215
- letter-spacing: 0.08em;
216
- padding: 3px 8px;
217
- border-radius: 4px;
218
- text-transform: uppercase;
219
  }
220
  .s1 .stage-badge { background: #dde3f0; color: #3a4a6b; }
221
  .s2 .stage-badge { background: #f0dcc8; color: #7a4010; }
222
  .s3 .stage-badge { background: #c0392b; color: #fff; }
 
223
 
224
- .stage-title {
225
- font-size: 13px;
226
- font-weight: 600;
227
- color: #1a1a2e;
228
- }
229
-
230
- /* Content inside cards */
231
  .tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
232
  .tag {
233
- font-family: 'IBM Plex Mono', monospace;
234
- font-size: 11px;
235
- background: #e8ecf5;
236
- color: #2c3e60;
237
- padding: 3px 8px;
238
- border-radius: 4px;
239
- white-space: nowrap;
240
  }
241
- .s2 .tag { background: #f5e8d8; color: #6b3a10; }
242
 
243
  .behavior-row {
244
- display: grid;
245
- grid-template-columns: 100px 1fr;
246
- gap: 4px 10px;
247
- margin-top: 2px;
248
- font-size: 12px;
249
- line-height: 1.5;
250
  }
251
  .bkey {
252
- font-family: 'IBM Plex Mono', monospace;
253
- font-size: 11px;
254
- font-weight: 600;
255
- color: #9b6a3a;
256
- padding-top: 1px;
257
  }
258
  .bval { color: #3a2a1a; }
259
 
260
- /* Prediction block */
261
- .pred-block { margin-top: 8px; }
262
  .pred-label {
263
- font-size: 11px;
264
- font-family: 'IBM Plex Mono', monospace;
265
- color: #888;
266
- text-transform: uppercase;
267
- letter-spacing: 0.06em;
268
- margin-bottom: 4px;
269
- }
270
- .pred-value {
271
- font-size: 22px;
272
- font-weight: 600;
273
- color: #c0392b;
274
- letter-spacing: -0.01em;
275
- margin-bottom: 8px;
276
- }
277
- .confidence-bar-wrap {
278
- display: flex;
279
- align-items: center;
280
- gap: 10px;
281
- margin-bottom: 10px;
282
- }
283
- .confidence-bar-bg {
284
- flex: 1;
285
- height: 6px;
286
- background: #f0d0cf;
287
- border-radius: 3px;
288
- overflow: hidden;
289
- }
290
- .confidence-bar-fill {
291
- height: 100%;
292
- background: linear-gradient(90deg, #e74c3c, #8b0000);
293
- border-radius: 3px;
294
- transition: width 0.8s ease;
295
- }
296
- .confidence-label {
297
- font-family: 'IBM Plex Mono', monospace;
298
- font-size: 11px;
299
- color: #c0392b;
300
- font-weight: 600;
301
- white-space: nowrap;
302
- }
303
- .reasoning-text {
304
- font-size: 12px;
305
- color: #4a2a2a;
306
- line-height: 1.6;
307
- border-left: 3px solid #e8c0be;
308
- padding-left: 10px;
309
- margin-top: 6px;
310
- }
311
- .alternatives {
312
- margin-top: 10px;
313
- font-size: 11px;
314
- font-family: 'IBM Plex Mono', monospace;
315
- color: #999;
316
  }
 
 
 
 
 
 
 
317
  .alternatives span { color: #c0392b; opacity: 0.7; }
318
 
319
- /* Arrow connector */
320
- .chain-arrow {
321
- display: flex;
322
- flex-direction: column;
323
- align-items: center;
324
- margin: 0;
325
- padding: 4px 0;
326
- gap: 0;
327
- }
328
- .arrow-line {
329
- width: 2px;
330
- height: 18px;
331
- background: linear-gradient(180deg, #c8d0e0, #e8c9a8);
332
- }
333
  .arrow-label {
334
- font-family: 'IBM Plex Mono', monospace;
335
- font-size: 10px;
336
- color: #aaa;
337
- letter-spacing: 0.06em;
338
- text-transform: uppercase;
339
- background: white;
340
- padding: 2px 8px;
341
- border: 1px solid #e0e0e0;
342
- border-radius: 10px;
343
- margin: 2px 0;
344
- }
345
- .arrow-tip {
346
- width: 0; height: 0;
347
- border-left: 5px solid transparent;
348
- border-right: 5px solid transparent;
349
- border-top: 7px solid #e8c9a8;
350
  }
 
 
 
 
351
 
352
- /* Waiting state */
353
- .waiting-dot {
354
- display: inline-block;
355
- width: 7px; height: 7px;
356
- border-radius: 50%;
357
- background: #ccc;
358
- margin: 0 2px;
359
- animation: pulse 1.2s ease-in-out infinite;
360
  }
361
- .waiting-dot:nth-child(2) { animation-delay: 0.2s; }
362
- .waiting-dot:nth-child(3) { animation-delay: 0.4s; }
363
- @keyframes pulse {
364
- 0%, 100% { opacity: 0.3; transform: scale(0.8); }
365
  50% { opacity: 1; transform: scale(1.1); }
366
  }
367
  </style>
368
  """
369
 
370
- def _waiting_dots():
371
- return '<span class="waiting-dot"></span><span class="waiting-dot"></span><span class="waiting-dot"></span>'
372
 
373
- def render_chain(s1_text="", s2_text="", s3_text="", status="idle"):
374
- """
375
- status: idle | running1 | running2 | running3 | done
376
- """
 
 
377
  s1_active = status in ("running1", "running2", "running3", "done")
378
  s2_active = status in ("running2", "running3", "done")
379
  s3_active = status in ("running3", "done")
380
 
381
- # ── Stage 1 content ──────────────────────────────────────────────────────
382
  if status == "running1":
383
- s1_content = f'<div style="padding:8px 0; color:#888; font-size:13px;">Extracting features {_waiting_dots()}</div>'
384
  elif s1_text:
385
- # Parse tags from the response β€” pull out short bullet points as tags
386
  tags = []
387
  for line in s1_text.splitlines():
388
  line = line.strip().lstrip("-").strip()
389
- if line and len(line) < 60 and not line.endswith(":"):
390
  tags.append(line)
391
- if len(tags) >= 8:
392
  break
393
- tag_html = "".join(f'<span class="tag">{t}</span>' for t in tags[:8])
394
- s1_content = f'<div class="tag-row">{tag_html}</div>'
 
395
  else:
396
- s1_content = '<div style="font-size:12px;color:#bbb;padding:6px 0;">Run inference to see results</div>'
397
 
398
- # ── Stage 2 content ──────────────────────────────────────────────────────
399
- BEHAVIOR_KEYS = ["SCHEDULE", "ECONOMIC", "SOCIAL", "LIFESTYLE", "STABILITY"]
400
  if status == "running2":
401
- s2_content = f'<div style="padding:8px 0; color:#a06030; font-size:13px;">Analyzing behavior {_waiting_dots()}</div>'
402
  elif s2_text:
403
  rows_html = ""
404
- for key in BEHAVIOR_KEYS:
405
- pattern = rf"{key}[:\s]+(.+)"
406
- m = re.search(pattern, s2_text, re.IGNORECASE)
407
  val = m.group(1).strip().rstrip(".") if m else "β€”"
408
- if len(val) > 80:
409
- val = val[:77] + "..."
410
  rows_html += f'<div class="bkey">{key}</div><div class="bval">{val}</div>'
411
  s2_content = f'<div class="behavior-row">{rows_html}</div>'
412
  else:
413
- s2_content = '<div style="font-size:12px;color:#bbb;padding:6px 0;">Run inference to see results</div>'
414
 
415
- # ── Stage 3 content ──────────────────────────────────────────────────────
416
  if status == "running3":
417
- s3_content = f'<div style="padding:8px 0; color:#c0392b; font-size:13px;">Inferring demographics {_waiting_dots()}</div>'
418
  elif s3_text:
419
- # Parse structured output
420
  pred = conf_raw = reasoning = alts = ""
421
  for line in s3_text.splitlines():
422
  line = line.strip()
@@ -428,18 +315,12 @@ def render_chain(s1_text="", s2_text="", s3_text="", status="idle"):
428
  reasoning = line.replace("INCOME_REASONING:", "").strip()
429
  elif line.startswith("ALTERNATIVES:"):
430
  alts = line.replace("ALTERNATIVES:", "").strip()
431
-
432
- # Confidence bar
433
  try:
434
  conf_int = int(re.search(r"\d", conf_raw).group())
435
  except:
436
  conf_int = 3
437
  bar_pct = conf_int * 20
438
-
439
- alts_html = ""
440
- if alts:
441
- alts_html = f'<div class="alternatives">Also possible: <span>{alts}</span></div>'
442
-
443
  s3_content = f"""
444
  <div class="pred-block">
445
  <div class="pred-label">Income Prediction</div>
@@ -450,16 +331,16 @@ def render_chain(s1_text="", s2_text="", s3_text="", status="idle"):
450
  </div>
451
  <div class="confidence-label">Confidence {conf_int}/5</div>
452
  </div>
453
- <div class="reasoning-text">{reasoning or s3_text[:200]}</div>
454
  {alts_html}
455
  </div>"""
456
  else:
457
- s3_content = '<div style="font-size:12px;color:#bbb;padding:6px 0;">Run inference to see results</div>'
458
 
459
  def card(cls, badge, title, content, active):
460
- dim_cls = "active" if active else "dim"
461
  return f"""
462
- <div class="stage-card {cls} {dim_cls}">
463
  <div class="stage-header">
464
  <span class="stage-badge">{badge}</span>
465
  <span class="stage-title">{title}</span>
@@ -468,16 +349,16 @@ def render_chain(s1_text="", s2_text="", s3_text="", status="idle"):
468
  </div>"""
469
 
470
  def arrow(label, active):
471
- opacity = "1" if active else "0.3"
472
  return f"""
473
- <div class="chain-arrow" style="opacity:{opacity}">
474
  <div class="arrow-line"></div>
475
  <div class="arrow-label">{label}</div>
476
  <div class="arrow-line"></div>
477
  <div class="arrow-tip"></div>
478
  </div>"""
479
 
480
- html = CHAIN_CSS + '<div class="hicotraj-chain">'
481
  html += card("s1", "Stage 1", "Factual Feature Extraction", s1_content, s1_active)
482
  html += arrow("behavioral abstraction", s2_active)
483
  html += card("s2", "Stage 2", "Behavioral Pattern Analysis", s2_content, s2_active)
@@ -546,51 +427,23 @@ def on_select(agent_id):
546
  map_html = build_map(agent_sp)
547
  demo_text = build_demo_text(agent_demo)
548
  raw_text = build_mobility_summary(agent_sp) + "\n\n" + build_weekly_checkin(agent_sp)
549
- chain_html = render_chain(status="idle")
550
 
551
  return map_html, raw_text, demo_text, chain_html
552
 
553
 
554
- def run_inference(agent_id, hf_token):
555
- if not hf_token or not hf_token.strip():
556
- yield render_chain(s3_text="⚠️ Please enter your Hugging Face token first.", status="done")
557
- return
558
-
559
  agent_id = int(agent_id)
560
- agent_sp = sp[sp["agent_id"] == agent_id].sort_values("start_datetime")
561
- traj_text = build_mobility_summary(agent_sp) + "\n\n" + build_weekly_checkin(agent_sp)
562
-
563
- try:
564
- client = InferenceClient(token=hf_token.strip())
565
-
566
- yield render_chain(status="running1")
567
- s1 = call_llm(client, STEP1_SYSTEM, traj_text, max_tokens=400)
568
-
569
- yield render_chain(s1_text=s1, status="running2")
570
- s2_input = f"Features:\n{s1}\n\nNow analyze behavioral patterns."
571
- s2 = call_llm(client, STEP2_SYSTEM, s2_input, max_tokens=300)
572
-
573
- yield render_chain(s1_text=s1, s2_text=s2, status="running3")
574
- s3_input = f"Features:\n{s1}\n\nBehavioral analysis:\n{s2}\n\nNow infer income."
575
- s3 = call_llm(client, STEP3_SYSTEM, s3_input, max_tokens=300)
576
 
577
- yield render_chain(s1_text=s1, s2_text=s2, s3_text=s3, status="done")
578
-
579
- except Exception as e:
580
- yield render_chain(s3_text=f"❌ Error: {str(e)}", status="done")
581
-
582
-
583
- def call_llm(client, system_prompt, user_content, max_tokens=400):
584
- response = client.chat.completions.create(
585
- model=MODEL_ID,
586
- messages=[
587
- {"role": "system", "content": system_prompt},
588
- {"role": "user", "content": user_content},
589
- ],
590
- max_tokens=max_tokens,
591
- temperature=0.3,
592
- )
593
- return response.choices[0].message.content.strip()
594
 
595
 
596
  # ── UI ────────────────────────────────────────────────────────────────────────
@@ -599,14 +452,6 @@ with gr.Blocks(title="HiCoTraj Demo", theme=gr.themes.Soft()) as app:
599
  gr.Markdown("## HiCoTraj β€” Trajectory Visualization & Hierarchical CoT Demo")
600
  gr.Markdown("*Zero-Shot Demographic Reasoning via Hierarchical Chain-of-Thought Prompting from Trajectory*")
601
 
602
- with gr.Row():
603
- hf_token_box = gr.Textbox(
604
- label="Hugging Face Token",
605
- placeholder="hf_...",
606
- type="password",
607
- scale=2
608
- )
609
-
610
  with gr.Row():
611
  agent_dd = gr.Dropdown(
612
  choices=[str(a) for a in sample_agents],
@@ -621,8 +466,6 @@ with gr.Blocks(title="HiCoTraj Demo", theme=gr.themes.Soft()) as app:
621
  )
622
 
623
  with gr.Row():
624
-
625
- # LEFT: map + NUMOSIM data
626
  with gr.Column(scale=1):
627
  gr.Markdown("### Trajectory Map")
628
  map_out = gr.HTML()
@@ -632,11 +475,10 @@ with gr.Blocks(title="HiCoTraj Demo", theme=gr.themes.Soft()) as app:
632
  label="Mobility Summary + Weekly Check-in"
633
  )
634
 
635
- # RIGHT: reasoning chain
636
  with gr.Column(scale=1):
637
  gr.Markdown("### Hierarchical Chain-of-Thought Reasoning")
638
- run_btn = gr.Button("β–Ά Run HiCoTraj Inference", variant="primary")
639
- chain_out = gr.HTML(value=render_chain(status="idle"))
640
 
641
  agent_dd.change(
642
  fn=on_select, inputs=agent_dd,
@@ -647,9 +489,8 @@ with gr.Blocks(title="HiCoTraj Demo", theme=gr.themes.Soft()) as app:
647
  outputs=[map_out, raw_out, demo_label, chain_out]
648
  )
649
  run_btn.click(
650
- fn=run_inference,
651
- inputs=[agent_dd, hf_token_box],
652
- outputs=[chain_out]
653
  )
654
 
655
  if __name__ == "__main__":
 
4
  import numpy as np
5
  import os
6
  import re
7
+ import json
8
 
9
  BASE = os.path.dirname(os.path.abspath(__file__))
10
  STAY_POINTS = os.path.join(BASE, "data", "stay_points_sampled.csv")
11
  POI_PATH = os.path.join(BASE, "data", "poi_sampled.csv")
12
  DEMO_PATH = os.path.join(BASE, "data", "demographics_sampled.csv")
13
+ COT_PATH = os.path.join(BASE, "data", "cot_results.json")
 
14
 
15
  SEX_MAP = {1:"Male", 2:"Female", -8:"Unknown", -7:"Prefer not to answer"}
16
  EDU_MAP = {1:"Less than HS", 2:"HS Graduate/GED", 3:"Some College/Associate",
 
49
  return str(x)
50
 
51
  sp["act_label"] = sp["act_types"].apply(parse_act_types)
52
+
53
+ # Load CoT JSON (optional)
54
+ cot_by_agent = {}
55
+ if os.path.exists(COT_PATH):
56
+ with open(COT_PATH, "r") as f:
57
+ cot_raw = json.load(f)
58
+ for result in cot_raw.get("inference_results", []):
59
+ cot_by_agent[result["agent_id"]] = result
60
+ print(f"Loaded CoT for {len(cot_by_agent)} agents.")
61
+
62
  sample_agents = sorted(sp["agent_id"].unique().tolist())
63
  print(f"Ready. {len(sample_agents)} agents loaded.")
64
 
65
 
66
+ # ── Mock CoT (fallback when agent not in JSON) ────────────────────────────────
67
+
68
+ MOCK_S1 = """LOCATION INVENTORY:
69
+ - Top venues: residence (36 visits), Clinton Mobile Estates (9 visits), 7-Eleven (8 visits)
70
+ - Price level: budget (7-Eleven, car wash) and mid-range (Euro Caffe, Pepper Shaker Cafe)
71
+ - Neighborhood: residential and commercial urban mix
72
+
73
+ TEMPORAL PATTERNS:
74
+ - Active hours: 09:00-23:00
75
+ - Weekday/Weekend: 66% weekday, 34% weekend
76
+ - Routine: consistent morning start times
77
+
78
+ SEQUENCE:
79
+ - Typical chain: Home to Exercise/Work to Home
80
+ - Notable pattern: weekend religious visits every Sunday morning"""
81
+
82
+ MOCK_S2 = """SCHEDULE: Fixed weekday routine with flexible afternoon activities
83
+ ECONOMIC: Budget-conscious with occasional mid-range dining
84
+ SOCIAL: Community-engaged through regular religious attendance
85
+ LIFESTYLE: Urban working-class with active recreational habits
86
+ STABILITY: Highly consistent 4-week pattern with minimal deviation"""
87
+
88
+ MOCK_S3 = """INCOME_PREDICTION: Middle ($35k-$75k)
89
+ INCOME_CONFIDENCE: 4
90
+ INCOME_REASONING: Frequent budget venue visits (7-Eleven, self-service car wash) signal cost awareness, while occasional mid-range dining and stable employment-like patterns at Clinton Mobile Estates suggest a steady middle income. No luxury venue signals detected.
91
+ ALTERNATIVES: Low ($15k-$35k) | Upper-Middle ($75k-$125k)"""
92
+
93
+
94
+ def get_cot(agent_id):
95
+ """Return (s1, s2, s3) text for agent, falling back to mock."""
96
+ result = cot_by_agent.get(agent_id)
97
+ if result:
98
+ s1 = result.get("step1_response", MOCK_S1)
99
+ s2 = result.get("step2_response", MOCK_S2)
100
+ s3 = result.get("step3_response", MOCK_S3)
101
+ else:
102
+ s1, s2, s3 = MOCK_S1, MOCK_S2, MOCK_S3
103
+ return s1, s2, s3
104
+
105
+
106
  # ── Mobility text builders ────────────────────────────────────────────────────
107
 
108
  def build_mobility_summary(agent_sp):
 
139
  return "night"
140
  agent_sp2["tod"] = agent_sp2["hour"].apply(tod)
141
  tod_pct = (agent_sp2["tod"].value_counts(normalize=True) * 100).round(0).astype(int)
 
142
  agent_sp2["is_weekend"] = agent_sp2["start_datetime"].dt.dayofweek >= 5
143
  wd_pct = int((~agent_sp2["is_weekend"]).mean() * 100)
144
 
 
169
  return "\n".join(lines)
170
 
171
 
172
+ # ── HTML reasoning chain ──────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  CHAIN_CSS = """
175
  <style>
176
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap');
177
 
178
+ .hicotraj-chain { font-family: 'IBM Plex Sans', sans-serif; padding: 12px 4px; }
 
 
 
 
179
 
 
180
  .stage-card {
181
+ border-radius: 10px; padding: 16px 18px; margin-bottom: 0;
182
+ transition: opacity 0.4s, filter 0.4s;
 
 
 
183
  }
184
+ .stage-card.dim { opacity: 0.32; filter: grayscale(0.5); }
185
+ .stage-card.active { opacity: 1; filter: none; }
186
 
187
  .stage-card.s1 { background: #f8f9fc; border: 1.5px solid #c8d0e0; }
188
  .stage-card.s2 { background: #fdf6f0; border: 1.5px solid #e8c9a8; }
189
  .stage-card.s3 { background: #fff8f8; border: 2px solid #c0392b; }
190
 
191
+ .stage-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
 
 
 
 
 
192
  .stage-badge {
193
  font-family: 'IBM Plex Mono', monospace;
194
+ font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
195
+ padding: 3px 8px; border-radius: 4px; text-transform: uppercase;
 
 
 
 
196
  }
197
  .s1 .stage-badge { background: #dde3f0; color: #3a4a6b; }
198
  .s2 .stage-badge { background: #f0dcc8; color: #7a4010; }
199
  .s3 .stage-badge { background: #c0392b; color: #fff; }
200
+ .stage-title { font-size: 13px; font-weight: 600; color: #1a1a2e; }
201
 
 
 
 
 
 
 
 
202
  .tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
203
  .tag {
204
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px;
205
+ background: #e8ecf5; color: #2c3e60;
206
+ padding: 3px 8px; border-radius: 4px; white-space: nowrap;
 
 
 
 
207
  }
 
208
 
209
  .behavior-row {
210
+ display: grid; grid-template-columns: 100px 1fr;
211
+ gap: 4px 10px; margin-top: 2px; font-size: 12px; line-height: 1.6;
 
 
 
 
212
  }
213
  .bkey {
214
+ font-family: 'IBM Plex Mono', monospace; font-size: 11px;
215
+ font-weight: 600; color: #9b6a3a; padding-top: 1px;
 
 
 
216
  }
217
  .bval { color: #3a2a1a; }
218
 
219
+ .pred-block { margin-top: 4px; }
 
220
  .pred-label {
221
+ font-size: 11px; font-family: 'IBM Plex Mono', monospace; color: #888;
222
+ text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
+ .pred-value { font-size: 22px; font-weight: 600; color: #c0392b; margin-bottom: 8px; }
225
+ .confidence-bar-wrap { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
226
+ .confidence-bar-bg { flex: 1; height: 6px; background: #f0d0cf; border-radius: 3px; overflow: hidden; }
227
+ .confidence-bar-fill { height: 100%; background: linear-gradient(90deg, #e74c3c, #8b0000); border-radius: 3px; }
228
+ .confidence-label { font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #c0392b; font-weight: 600; white-space: nowrap; }
229
+ .reasoning-text { font-size: 12px; color: #4a2a2a; line-height: 1.6; border-left: 3px solid #e8c0be; padding-left: 10px; margin-top: 6px; }
230
+ .alternatives { margin-top: 10px; font-size: 11px; font-family: 'IBM Plex Mono', monospace; color: #999; }
231
  .alternatives span { color: #c0392b; opacity: 0.7; }
232
 
233
+ .chain-arrow { display: flex; flex-direction: column; align-items: center; padding: 4px 0; transition: opacity 0.4s; }
234
+ .arrow-line { width: 2px; height: 16px; background: #d0c0b0; }
 
 
 
 
 
 
 
 
 
 
 
 
235
  .arrow-label {
236
+ font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: #aaa;
237
+ letter-spacing: 0.06em; text-transform: uppercase;
238
+ background: white; padding: 2px 8px; border: 1px solid #e0e0e0; border-radius: 10px; margin: 2px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  }
240
+ .arrow-tip { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #d0c0b0; }
241
+
242
+ .thinking { font-size: 13px; color: #888; padding: 8px 0; }
243
+ .empty-hint { font-size: 12px; color: #ccc; padding: 6px 0; }
244
 
245
+ .wd {
246
+ display: inline-block; width: 6px; height: 6px; border-radius: 50%;
247
+ background: currentColor; margin: 0 2px; opacity: 0.3;
248
+ animation: wd-pulse 1.2s ease-in-out infinite;
 
 
 
 
249
  }
250
+ .wd:nth-child(2) { animation-delay: 0.2s; }
251
+ .wd:nth-child(3) { animation-delay: 0.4s; }
252
+ @keyframes wd-pulse {
253
+ 0%, 100% { opacity: 0.2; transform: scale(0.8); }
254
  50% { opacity: 1; transform: scale(1.1); }
255
  }
256
  </style>
257
  """
258
 
 
 
259
 
260
+ def _dots():
261
+ return '<span class="wd"></span><span class="wd"></span><span class="wd"></span>'
262
+
263
+
264
+ def render_chain(s1_text, s2_text, s3_text, status="done"):
265
+ # status: idle | running1 | running2 | running3 | done
266
  s1_active = status in ("running1", "running2", "running3", "done")
267
  s2_active = status in ("running2", "running3", "done")
268
  s3_active = status in ("running3", "done")
269
 
270
+ # ── Stage 1 ───────────────────────────────────────────────────────────────
271
  if status == "running1":
272
+ s1_content = f'<div class="thinking">Extracting features {_dots()}</div>'
273
  elif s1_text:
 
274
  tags = []
275
  for line in s1_text.splitlines():
276
  line = line.strip().lstrip("-").strip()
277
+ if line and len(line) < 65 and not line.endswith(":"):
278
  tags.append(line)
279
+ if len(tags) >= 9:
280
  break
281
+ s1_content = '<div class="tag-row">' + \
282
+ "".join(f'<span class="tag">{t}</span>' for t in tags[:9]) + \
283
+ '</div>'
284
  else:
285
+ s1_content = '<div class="empty-hint">Press β–Ά Run HiCoTraj to start</div>'
286
 
287
+ # ── Stage 2 ───────────────────────────────────────────────────────────────
288
+ KEYS = ["SCHEDULE", "ECONOMIC", "SOCIAL", "LIFESTYLE", "STABILITY"]
289
  if status == "running2":
290
+ s2_content = f'<div class="thinking" style="color:#a06030">Analyzing behavior {_dots()}</div>'
291
  elif s2_text:
292
  rows_html = ""
293
+ for key in KEYS:
294
+ m = re.search(rf"{key}[:\s]+(.+)", s2_text, re.IGNORECASE)
 
295
  val = m.group(1).strip().rstrip(".") if m else "β€”"
296
+ if len(val) > 85:
297
+ val = val[:82] + "..."
298
  rows_html += f'<div class="bkey">{key}</div><div class="bval">{val}</div>'
299
  s2_content = f'<div class="behavior-row">{rows_html}</div>'
300
  else:
301
+ s2_content = '<div class="empty-hint">Waiting...</div>'
302
 
303
+ # ── Stage 3 ───────────────────────────────────────────────────────────────
304
  if status == "running3":
305
+ s3_content = f'<div class="thinking" style="color:#c0392b">Inferring demographics {_dots()}</div>'
306
  elif s3_text:
 
307
  pred = conf_raw = reasoning = alts = ""
308
  for line in s3_text.splitlines():
309
  line = line.strip()
 
315
  reasoning = line.replace("INCOME_REASONING:", "").strip()
316
  elif line.startswith("ALTERNATIVES:"):
317
  alts = line.replace("ALTERNATIVES:", "").strip()
 
 
318
  try:
319
  conf_int = int(re.search(r"\d", conf_raw).group())
320
  except:
321
  conf_int = 3
322
  bar_pct = conf_int * 20
323
+ alts_html = f'<div class="alternatives">Also possible: <span>{alts}</span></div>' if alts else ""
 
 
 
 
324
  s3_content = f"""
325
  <div class="pred-block">
326
  <div class="pred-label">Income Prediction</div>
 
331
  </div>
332
  <div class="confidence-label">Confidence {conf_int}/5</div>
333
  </div>
334
+ <div class="reasoning-text">{reasoning}</div>
335
  {alts_html}
336
  </div>"""
337
  else:
338
+ s3_content = '<div class="empty-hint">Waiting...</div>'
339
 
340
  def card(cls, badge, title, content, active):
341
+ dim = "active" if active else "dim"
342
  return f"""
343
+ <div class="stage-card {cls} {dim}">
344
  <div class="stage-header">
345
  <span class="stage-badge">{badge}</span>
346
  <span class="stage-title">{title}</span>
 
349
  </div>"""
350
 
351
  def arrow(label, active):
352
+ op = "1" if active else "0.25"
353
  return f"""
354
+ <div class="chain-arrow" style="opacity:{op}">
355
  <div class="arrow-line"></div>
356
  <div class="arrow-label">{label}</div>
357
  <div class="arrow-line"></div>
358
  <div class="arrow-tip"></div>
359
  </div>"""
360
 
361
+ html = CHAIN_CSS + '<div class="hicotraj-chain">'
362
  html += card("s1", "Stage 1", "Factual Feature Extraction", s1_content, s1_active)
363
  html += arrow("behavioral abstraction", s2_active)
364
  html += card("s2", "Stage 2", "Behavioral Pattern Analysis", s2_content, s2_active)
 
427
  map_html = build_map(agent_sp)
428
  demo_text = build_demo_text(agent_demo)
429
  raw_text = build_mobility_summary(agent_sp) + "\n\n" + build_weekly_checkin(agent_sp)
430
+ chain_html = render_chain("", "", "", status="idle")
431
 
432
  return map_html, raw_text, demo_text, chain_html
433
 
434
 
435
+ def run_reveal(agent_id):
436
+ import time
 
 
 
437
  agent_id = int(agent_id)
438
+ s1, s2, s3 = get_cot(agent_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
+ yield render_chain("", "", "", status="running1")
441
+ time.sleep(0.8)
442
+ yield render_chain(s1, "", "", status="running2")
443
+ time.sleep(0.8)
444
+ yield render_chain(s1, s2, "", status="running3")
445
+ time.sleep(0.8)
446
+ yield render_chain(s1, s2, s3, status="done")
 
 
 
 
 
 
 
 
 
 
447
 
448
 
449
  # ── UI ────────────────────────────────────────────────────────────────────────
 
452
  gr.Markdown("## HiCoTraj β€” Trajectory Visualization & Hierarchical CoT Demo")
453
  gr.Markdown("*Zero-Shot Demographic Reasoning via Hierarchical Chain-of-Thought Prompting from Trajectory*")
454
 
 
 
 
 
 
 
 
 
455
  with gr.Row():
456
  agent_dd = gr.Dropdown(
457
  choices=[str(a) for a in sample_agents],
 
466
  )
467
 
468
  with gr.Row():
 
 
469
  with gr.Column(scale=1):
470
  gr.Markdown("### Trajectory Map")
471
  map_out = gr.HTML()
 
475
  label="Mobility Summary + Weekly Check-in"
476
  )
477
 
 
478
  with gr.Column(scale=1):
479
  gr.Markdown("### Hierarchical Chain-of-Thought Reasoning")
480
+ run_btn = gr.Button("β–Ά Run HiCoTraj", variant="primary")
481
+ chain_out = gr.HTML(value=render_chain("", "", "", status="idle"))
482
 
483
  agent_dd.change(
484
  fn=on_select, inputs=agent_dd,
 
489
  outputs=[map_out, raw_out, demo_label, chain_out]
490
  )
491
  run_btn.click(
492
+ fn=run_reveal, inputs=agent_dd,
493
+ outputs=chain_out
 
494
  )
495
 
496
  if __name__ == "__main__":