-
Notifications
You must be signed in to change notification settings - Fork 159
Expand file tree
/
Copy pathprompts_activity.py
More file actions
1765 lines (1580 loc) · 108 KB
/
prompts_activity.py
File metadata and controls
1765 lines (1580 loc) · 108 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Multi-language prompts and labels for the activity tracker.
Lives under ``config/prompts/prompts_*`` per the project's i18n convention —
**all** multi-language strings must live here, not in regular code, so
that adding a new language is a single-file pass over ``config/`` and
nothing slips through. The prompt-hygiene linter
(``scripts/check_prompt_hygiene.py``) only catches *flat*
``{lang_code: str}`` dicts; nested-dict tables (``{lang: {key: str}}``)
must be moved here by convention even though the linter wouldn't fire.
What ships here:
Flat ``{lang_code: str}`` maps (resolved via ``_loc(MAP, lang)``):
* ``ACTIVITY_GUESS_PROMPTS`` — emotion-tier system prompt that asks
the model to soft-score the user's current activity state and write
a one-sentence narrative. Consumed by
``main_logic/activity/llm_enrichment.py:call_activity_guess``.
* ``OPEN_THREADS_PROMPTS`` — emotion-tier system prompt that detects
semantically open threads (promises, abandoned mid-sentences, etc.)
beyond the question-mark heuristic. Consumed by
``main_logic/activity/llm_enrichment.py:call_open_threads``.
* ``TOPIC_CANDIDATE_PROMPTS`` — background-only prompt that turns recent
conversation snippets into 1-2 summarized deep-topic hooks. Consumed by
``main_logic/activity/llm_enrichment.py:call_topic_candidates``.
* ``OS_DEGRADED_MARKER`` — short bracketed text appended to the
state-section header when the backend can't read the user's OS
signals. Consumed by
``main_logic/activity/snapshot.py:format_activity_state_section``.
Nested ``{lang_code: {key: str}}`` tables (resolved via
``MAP.get(lang, MAP['en']).get(key, ...)``); used by
``format_activity_state_section`` to render the snapshot:
* ``ACTIVITY_STATE_LABELS`` — human-readable label for each
``ActivityState`` (e.g. ``focused_work`` → ``专注工作中``).
* ``ACTIVITY_PROPENSITY_DIRECTIVES`` — short directive sentence for
each ``Propensity`` (e.g. ``restricted_screen_only`` →
``只就屏幕内容轻聊一句``).
* ``ACTIVITY_REASON_TEMPLATES`` — ``str.format``-able templates for
each structured reason code emitted by the state machine.
* ``ACTIVITY_STATE_SECTION_LABELS`` — header / footer / period names
/ time-relative phrases used to assemble the final state section.
"""
from __future__ import annotations
# ── Activity guess + soft scores (emotion-tier) ─────────────────────
ACTIVITY_GUESS_PROMPTS: dict[str, str] = {
"zh": """你是一个用户活动分析助手。基于下方的系统信号和最近对话片段,对用户当前的活动状态做软评分,并写一句简短的活动叙述。
======以下为系统信号======
{signals}
======以上为系统信号======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======以下为规则系统的初判======
{rule_state}
======以上为规则系统的初判======
请输出严格的 JSON(不带 markdown 代码块),字段:
- "scores": 一个对象,键是状态名,值是 0.0-1.0 的浮点数(独立打分,不需要归一化)。允许的状态名:{state_keys}
- "guess": 一句话叙述用户当前在做什么,符合中文表达习惯,不超过 40 字
如果某状态完全不像,给 0.0;如果非常像,给接近 1.0。多个状态可以同时高分(例如同时在写代码和聊天)。
如果你的判断和"规则系统的初判"不同,按你看到的实际信号给分;规则只是参考,不必盲从。
输出示例:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "主人在 VS Code 里写代码,偶尔切到聊天软件回消息"}}""",
"en": """You are a user-activity analyst. Given the system signals and recent conversation snippets below, give soft scores for the user's current activity state and write a one-sentence narrative.
======Below is System signals======
{signals}
======Above is System signals======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======Below is Rule system's initial classification======
{rule_state}
======Above is Rule system's initial classification======
Output strict JSON (no markdown fences), with fields:
- "scores": object mapping state name to a 0.0-1.0 float (independent scoring, no normalization). Allowed states: {state_keys}
- "guess": one short sentence describing what the user is doing right now, max ~40 words
Give 0.0 for states that don't fit at all; close to 1.0 for very fitting ones. Multiple states can be high simultaneously (e.g. coding while chatting).
If you disagree with the rule classification, score based on the actual signals — the rule is just a reference, not gospel.
Example output:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "Master is coding in VS Code, occasionally switching to a chat app to reply"}}""",
"ja": """あなたはユーザー活動の分析助手です。下のシステム信号と最近の会話に基づき、ユーザーの現在の活動状態にソフトスコアを付けて、一文の活動叙述を書いてください。
======以下はシステム信号======
{signals}
======以上はシステム信号======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======以下はルール系の初期判定======
{rule_state}
======以上はルール系の初期判定======
厳密なJSON(markdownコードブロックなし)で出力してください:
- "scores": 状態名をキー、0.0〜1.0の浮動小数を値とするオブジェクト(独立スコア、正規化不要)。許可される状態:{state_keys}
- "guess": ユーザーが今何をしているかを表す一文、自然な日本語で40字以内
全く当てはまらない状態は0.0、非常に当てはまる状態は1.0近く。複数の状態が同時に高くてもOK。
ルール初期判定と意見が違う場合は、実際の信号に従ってください。ルールは参考に過ぎません。
出力例:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "ご主人はVS Codeでコーディング中、時々チャットアプリに切り替えて返信している"}}""",
"ko": """당신은 사용자 활동 분석 도우미입니다. 아래의 시스템 신호와 최근 대화 스니펫을 바탕으로 사용자의 현재 활동 상태에 소프트 점수를 매기고, 활동 서술 한 문장을 작성하세요.
======아래는 시스템 신호======
{signals}
======위는 시스템 신호======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======아래는 규칙 시스템의 초기 판정======
{rule_state}
======위는 규칙 시스템의 초기 판정======
엄격한 JSON으로 출력하세요 (markdown 코드 블록 없이). 필드:
- "scores": 상태명을 키로, 0.0-1.0 부동소수를 값으로 하는 객체 (독립 점수, 정규화 불필요). 허용 상태: {state_keys}
- "guess": 사용자가 지금 무엇을 하는지에 대한 한 문장, 자연스러운 한국어로 40자 이내
전혀 해당하지 않으면 0.0, 매우 해당하면 1.0 근처. 여러 상태가 동시에 높아도 됨.
규칙 초기 판정과 다르면 실제 신호에 따라 점수를 매기세요. 규칙은 참고일 뿐.
출력 예:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "주인님이 VS Code에서 코딩 중, 가끔 채팅 앱으로 전환해 답장 중"}}""",
"ru": """Вы — аналитик активности пользователя. Опираясь на сигналы системы и недавние реплики ниже, поставьте мягкие оценки текущему состоянию активности пользователя и напишите одно предложение-описание.
======Ниже Сигналы системы======
{signals}
======Выше Сигналы системы======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======Ниже Первоначальная классификация правил======
{rule_state}
======Выше Первоначальная классификация правил======
Выведите строгий JSON (без markdown-обрамления), поля:
- "scores": объект «название состояния → число 0.0-1.0» (независимые оценки, нормализация не нужна). Допустимые состояния: {state_keys}
- "guess": одно короткое предложение о том, что пользователь делает прямо сейчас, до ~40 слов
0.0 — состояние совсем не подходит; ближе к 1.0 — очень подходит. Несколько состояний могут быть одновременно высокими.
Если вы не согласны с классификацией правил — оценивайте по реальным сигналам. Правила — лишь ориентир.
Пример вывода:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "Хозяин кодит в VS Code, иногда переключается в чат для ответа"}}""",
"es": """Eres un analista de actividad del usuario. Con las señales del sistema y los fragmentos recientes de conversación, asigna puntuaciones suaves al estado actual de actividad del usuario y escribe una narración de una frase.
======A continuación están las señales del sistema======
{signals}
======Fin de las señales del sistema======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======A continuación está la clasificación inicial del sistema de reglas======
{rule_state}
======Fin de la clasificación inicial del sistema de reglas======
Devuelve JSON estricto (sin bloques markdown), con campos:
- "scores": objeto que asigna nombre de estado a float 0.0-1.0 (puntuaciones independientes, sin normalización). Estados permitidos: {state_keys}
- "guess": una frase breve que describa qué hace el usuario ahora, máximo ~40 palabras
Da 0.0 a estados que no encajan; cerca de 1.0 a los que encajan muy bien. Varios estados pueden tener puntuación alta al mismo tiempo.
Si discrepas de la clasificación de reglas, puntúa según las señales reales; la regla es solo referencia.
Ejemplo:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "Master está programando en VS Code y a veces cambia a una app de chat para responder"}}""",
"pt": """Você é um analista de atividade do usuário. Com os sinais do sistema e trechos recentes da conversa, atribua pontuações suaves ao estado atual de atividade do usuário e escreva uma narrativa de uma frase.
======Abaixo estão os sinais do sistema======
{signals}
======Acima estão os sinais do sistema======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
======Abaixo está a classificação inicial do sistema de regras======
{rule_state}
======Acima está a classificação inicial do sistema de regras======
Retorne JSON estrito (sem blocos markdown), com campos:
- "scores": objeto que mapeia nome do estado para float 0.0-1.0 (pontuação independente, sem normalização). Estados permitidos: {state_keys}
- "guess": uma frase curta descrevendo o que o usuário está fazendo agora, máximo ~40 palavras
Dê 0.0 para estados que não combinam; perto de 1.0 para os que combinam muito. Vários estados podem ter pontuação alta ao mesmo tempo.
Se discordar da classificação de regras, pontue com base nos sinais reais; a regra é apenas referência.
Exemplo:
{{"scores": {{"focused_work": 0.7, "chatting": 0.2, "idle": 0.1, "gaming": 0.0, "casual_browsing": 0.0, "voice_engaged": 0.0}}, "guess": "Master está codando no VS Code e às vezes troca para um app de chat para responder"}}""",
}
# ── Background topic hook candidates ────────────────────────────────
TOPIC_CANDIDATE_PROMPTS: dict[str, str] = {
"zh": """你是一个陪伴产品的话题筛选助手。你的任务不是总结最近一句话,而是从“慢收集的全局证据 + 最近对话”里挑 1-2 个真的值得以后低频开口的深话题机会。
======以下为慢收集的全局证据======
{global_signals}
======以上为慢收集的全局证据======
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
要求:
- 不要复述用户原话,不要暴露“我分析了你的聊天记录”
- 只保留和用户近期兴趣、计划、纠结、情绪、选择强相关,而且能从全局证据里看出稳定性的点
- 不要把两个只是相邻出现的名词硬拼成一个话题;如果关联不自然,宁可不要输出
- 寒暄、语气词、很薄的短句、问卷式问题,一律给低优先级或不要输出
- 每个话题要像给角色的一张小抄:知道怎么自然开口,但最终开口仍交给角色生成
- 重点是关系深度,不是触发频率;宁可少,不要硬凑
- 如果这个话题适合联网补一点现实细节,给一个简短 search_query;查询词要围绕最稳定的关系点,不要围绕最近窗口里的偶然词
输出严格 JSON(不带 markdown 代码块):
{{"topics": [
{{
"interest": "整理后的关系话题,不超过30字",
"hook": "从什么角度接住,不超过45字",
"opening_intent": "开口风格,不超过35字",
"deepening_hint": "用户接话后的展开方向,不超过40字",
"why_now": "为什么现在值得轻轻接一下,不超过50字",
"search_query": "用于联网补现实细节的查询词;不需要联网就留空",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
评分:
- collection_score:这批慢收集证据整体是否够开一个深话题,低于 80 不要输出
- readiness:证据是否已经够稳定,低于 70 不要输出
- confidence:这个话题和用户的强相关程度,低于 55 不要输出
- risk:打扰、冒犯、误解、硬凑的风险,高于 65 不要输出
如果没有值得以后接的话题,输出 {{"topics": []}}。""",
"zh-TW": """你是陪伴產品的話題篩選助手。你的任務不是總結最近一句話,而是從「慢收集的全局證據 + 最近對話」裡挑 1-2 個真的值得以後低頻開口的深話題機會。
======以下為慢收集的全局證據======
{global_signals}
======以上為慢收集的全局證據======
======以下為最近對話(按時間順序)======
{conversation}
======以上為最近對話(按時間順序)======
要求:
- 所有文字欄位必須使用繁體中文;不要輸出英文話題
- 不要復述用戶原話,不要暴露「我分析了你的聊天記錄」
- 只保留和用戶近期興趣、計畫、糾結、情緒、選擇強相關,而且能從全局證據裡看出穩定性的點
- 不要把兩個只是相鄰出現的名詞硬拼成一個話題;如果關聯不自然,寧可不要輸出
- 寒暄、語氣詞、很薄的短句、問卷式問題,一律給低優先級或不要輸出
- 每個話題要像給角色的一張小抄:知道怎麼自然開口,但最終開口仍交給角色生成
- 重點是關係深度,不是觸發頻率;寧可少,不要硬湊
- 如果這個話題適合聯網補一點現實細節,給一個簡短 search_query;查詢詞也使用繁體中文,圍繞最穩定的關係點
輸出嚴格 JSON(不帶 markdown 代碼塊):
{{"topics": [
{{
"interest": "整理後的關係話題,不超過30字",
"hook": "從什麼角度接住,不超過45字",
"opening_intent": "開口風格,不超過35字",
"deepening_hint": "用戶接話後的展開方向,不超過40字",
"why_now": "為什麼現在值得輕輕接一下,不超過50字",
"search_query": "用於聯網補現實細節的查詢詞;不需要聯網就留空",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
評分:
- collection_score:這批慢收集證據整體是否夠開一個深話題,低於 80 不要輸出
- readiness:證據是否已經夠穩定,低於 70 不要輸出
- confidence:這個話題和用戶的強相關程度,低於 55 不要輸出
- risk:打擾、冒犯、誤解、硬湊的風險,高於 65 不要輸出
如果沒有值得以後接的話題,輸出 {{"topics": []}}。""",
"en": """You are a topic-screening assistant for a companionship product. Your job is not to summarize the last message, but to choose 1-2 genuinely worthwhile low-frequency topic opportunities from slow global evidence plus the recent conversation.
======Slow global evidence======
{global_signals}
======End slow global evidence======
======Recent conversation, chronological======
{conversation}
======End conversation======
Rules:
- Do not repeat the user's raw wording or reveal that chat logs were analyzed
- Keep only topics strongly tied to recent interests, plans, dilemmas, emotions, or choices, with visible stability in the global evidence
- Do not glue together two nouns just because they appeared near each other; if the association is not natural, output nothing
- Greetings, filler, thin short replies, and survey-like prompts should be low priority or omitted
- Each topic is a small note for the character: how to open naturally, not final copy
- Relationship depth matters more than trigger frequency; fewer is better than forced
- If a topic would benefit from a concrete online detail, provide a concise search_query. The query should target the stable relationship point, not an accidental recent keyword
Output strict JSON, no markdown fences:
{{"topics": [
{{
"interest": "summarized relationship topic, max 30 words",
"hook": "angle to naturally pick it up, max 45 words",
"opening_intent": "opening style, max 35 words",
"deepening_hint": "how to continue if the user responds, max 40 words",
"why_now": "why this is worth lightly picking up now, max 50 words",
"search_query": "query for online enrichment; empty if not needed",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
Scoring:
- collection_score: whether this slow-evidence batch is strong enough for one deeper hook; omit below 80
- readiness: whether the evidence is stable enough; omit below 70
- confidence: strength of connection to the user; omit below 55
- risk: interruption/offense/misread/forced-association risk; omit above 65
If nothing is worth keeping, output {{"topics": []}}.""",
"ja": """あなたはコンパニオン製品の話題選別アシスタントです。直近の一言を要約するのではなく、「ゆっくり集めた全体証拠 + 最近の会話」から、あとで低頻度で自然に切り出す価値がある深めの話題を1〜2個だけ選びます。
======ゆっくり集めた全体証拠======
{global_signals}
======全体証拠ここまで======
======最近の会話(時系列)======
{conversation}
======最近の会話ここまで======
ルール:
- すべての文字フィールドはユーザーの言語で、日本語ユーザーなら自然な日本語で書くこと
- ユーザーの原文をそのまま繰り返さない。「チャット履歴を分析した」と明かさない
- 最近の興味、予定、迷い、感情、選択に強く結びつき、全体証拠から安定して見える点だけ残す
- 近くに出ただけの名詞を無理につなげない。関連が自然でなければ出力しない
- あいさつ、相づち、薄い短文、アンケート風の問いは低優先度または除外
- 各話題はキャラクター用の短いメモ。最終的な口調はキャラクター側に任せる
- 大事なのは関係の深さで、頻度ではない。無理に埋めるより少なくする
- 現実の具体情報が少し役立つ話題なら、短い search_query を入れる。検索語もユーザーの言語で、安定した関係点に絞る
厳密な JSON だけを出力(markdown コードブロックなし):
{{"topics": [
{{
"interest": "整理した関係話題、30字以内",
"hook": "自然に拾う角度、45字以内",
"opening_intent": "切り出し方、35字以内",
"deepening_hint": "ユーザーが乗った後の広げ方、40字以内",
"why_now": "今そっと拾う理由、50字以内",
"search_query": "現実情報を補う検索語。不要なら空文字",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
スコア:
- collection_score:深い話題にできるだけの証拠量。80未満は出力しない
- readiness:証拠の安定度。70未満は出力しない
- confidence:ユーザーとの関連の強さ。55未満は出力しない
- risk:邪魔、失礼、誤読、こじつけのリスク。65超は出力しない
価値のある話題がなければ {{"topics": []}} を出力。""",
"ko": """당신은 동반자 제품의 화제 선별 도우미입니다. 최근 한마디를 요약하는 것이 아니라, "천천히 모은 전역 근거 + 최근 대화"에서 나중에 낮은 빈도로 자연스럽게 꺼낼 만한 깊은 화제 기회를 1-2개만 고릅니다.
======천천히 모은 전역 근거======
{global_signals}
======전역 근거 끝======
======최근 대화(시간순)======
{conversation}
======최근 대화 끝======
규칙:
- 모든 텍스트 필드는 사용자 언어로 작성하세요. 한국어 사용자라면 자연스러운 한국어로 출력하세요
- 사용자의 원문을 그대로 반복하지 말고, "대화 기록을 분석했다"고 드러내지 마세요
- 최근 관심사, 계획, 고민, 감정, 선택과 강하게 관련되고 전역 근거에서 안정성이 보이는 점만 남기세요
- 가까이 나온 명사 두 개를 억지로 붙이지 마세요. 연결이 자연스럽지 않으면 출력하지 마세요
- 인사, 추임새, 얇은 짧은 답, 설문 같은 질문은 낮은 우선순위로 두거나 제외하세요
- 각 화제는 캐릭터를 위한 짧은 메모입니다. 최종 말투는 캐릭터 생성 단계에 맡깁니다
- 중요한 것은 관계의 깊이이지 빈도가 아닙니다. 억지로 채우기보다 적게 출력하세요
- 현실 정보가 조금 도움이 될 화제라면 짧은 search_query를 넣으세요. 검색어도 사용자 언어로, 안정적인 관계점에 맞추세요
엄격한 JSON만 출력하세요(markdown 코드 블록 금지):
{{"topics": [
{{
"interest": "정리된 관계 화제, 30자 이내",
"hook": "자연스럽게 받아낼 각도, 45자 이내",
"opening_intent": "말을 여는 방식, 35자 이내",
"deepening_hint": "사용자가 반응한 뒤 이어갈 방향, 40자 이내",
"why_now": "지금 가볍게 꺼낼 만한 이유, 50자 이내",
"search_query": "현실 정보를 보충할 검색어. 필요 없으면 빈 문자열",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
점수:
- collection_score: 깊은 화제로 삼을 만큼 근거가 충분한가. 80 미만은 출력하지 않음
- readiness: 근거가 충분히 안정적인가. 70 미만은 출력하지 않음
- confidence: 사용자와의 관련 강도. 55 미만은 출력하지 않음
- risk: 방해, 무례함, 오해, 억지 연결 위험. 65 초과는 출력하지 않음
가치 있는 화제가 없으면 {{"topics": []}} 를 출력하세요.""",
"es": """Eres un asistente que selecciona temas para un producto de compañía. Tu tarea no es resumir el último mensaje, sino elegir 1-2 oportunidades de conversación profunda que valga la pena abrir con baja frecuencia a partir de evidencia global acumulada lentamente y la conversación reciente.
======Evidencia global acumulada lentamente======
{global_signals}
======Fin de la evidencia global======
======Conversación reciente, en orden cronológico======
{conversation}
======Fin de la conversación======
Reglas:
- Todos los campos de texto deben estar en el idioma del usuario; para usuarios en español, escribe en español natural
- No repitas literalmente lo que dijo el usuario ni reveles que analizaste su historial
- Conserva solo temas muy ligados a intereses, planes, dilemas, emociones o elecciones recientes, con estabilidad visible en la evidencia global
- No unas dos sustantivos solo porque aparecieron cerca; si la conexión no es natural, no outputes nada
- Saludos, muletillas, respuestas muy finas o preguntas tipo encuesta deben tener baja prioridad o omitirse
- Cada tema es una nota breve para el personaje: cómo abrir naturalmente, no el texto final
- Importa más la profundidad de la relación que la frecuencia; mejor pocos que forzados
- Si ayudaría un detalle real de internet, da un search_query breve en el idioma del usuario y centrado en el punto estable
Devuelve JSON estricto, sin bloques markdown:
{{"topics": [
{{
"interest": "tema relacional resumido, máximo 30 palabras",
"hook": "ángulo para retomarlo naturalmente, máximo 45 palabras",
"opening_intent": "estilo de apertura, máximo 35 palabras",
"deepening_hint": "cómo seguir si el usuario responde, máximo 40 palabras",
"why_now": "por qué vale la pena tocarlo ahora, máximo 50 palabras",
"search_query": "consulta para enriquecer con datos reales; vacío si no hace falta",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
Puntuación:
- collection_score: evidencia suficiente para un tema profundo; omite por debajo de 80
- readiness: estabilidad de la evidencia; omite por debajo de 70
- confidence: fuerza de la relación con el usuario; omite por debajo de 55
- risk: riesgo de molestar, ofender, malinterpretar o forzar; omite por encima de 65
Si no hay nada que valga la pena, devuelve {{"topics": []}}.""",
"pt": """Voce e um assistente de selecao de assuntos para um produto de companhia. Sua tarefa nao e resumir a ultima mensagem, mas escolher 1-2 oportunidades de conversa profunda que valem ser puxadas com baixa frequencia, usando evidencias globais coletadas aos poucos e a conversa recente.
======Evidencias globais coletadas aos poucos======
{global_signals}
======Fim das evidencias globais======
======Conversa recente, em ordem cronologica======
{conversation}
======Fim da conversa======
Regras:
- Todos os campos de texto devem estar no idioma do usuario; para usuarios em portugues, escreva em portugues natural
- Nao repita literalmente a fala do usuario nem revele que voce analisou historico de conversa
- Mantenha apenas temas muito ligados a interesses, planos, dilemas, emocoes ou escolhas recentes, com estabilidade visivel nas evidencias globais
- Nao junte dois substantivos so porque apareceram perto; se a ligacao nao for natural, nao outpute nada
- Cumprimentos, muletas, respostas muito finas ou perguntas com cara de questionario devem ter baixa prioridade ou ser omitidos
- Cada tema e uma nota curta para o personagem: como abrir naturalmente, nao o texto final
- O foco e profundidade de relacao, nao frequencia; melhor pouco do que forcado
- Se um detalhe real da internet ajudar, forneca um search_query curto no idioma do usuario e centrado no ponto estavel
Retorne JSON estrito, sem blocos markdown:
{{"topics": [
{{
"interest": "tema relacional resumido, maximo 30 palavras",
"hook": "angulo para retomar naturalmente, maximo 45 palavras",
"opening_intent": "estilo de abertura, maximo 35 palavras",
"deepening_hint": "como continuar se o usuario responder, maximo 40 palavras",
"why_now": "por que vale tocar nisso agora, maximo 50 palavras",
"search_query": "consulta para enriquecer com dados reais; vazio se nao precisar",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
Pontuacao:
- collection_score: evidencias suficientes para um tema profundo; omita abaixo de 80
- readiness: estabilidade das evidencias; omita abaixo de 70
- confidence: forca da relacao com o usuario; omita abaixo de 55
- risk: risco de incomodar, ofender, interpretar errado ou forcar; omita acima de 65
Se nada valer a pena, retorne {{"topics": []}}.""",
"ru": """Ты помощник по отбору тем для companion-продукта. Твоя задача не пересказывать последнее сообщение, а выбрать 1-2 действительно ценные возможности для редкого, естественного начала более глубокого разговора на основе медленно собранных общих сигналов и недавней переписки.
======Медленно собранные общие сигналы======
{global_signals}
======Конец общих сигналов======
======Недавняя переписка по порядку======
{conversation}
======Конец переписки======
Правила:
- Все текстовые поля должны быть на языке пользователя; для русскоязычного пользователя пиши естественно на русском
- Не повторяй слова пользователя дословно и не раскрывай, что анализировал историю чата
- Оставляй только темы, тесно связанные с недавними интересами, планами, сомнениями, эмоциями или выборами пользователя, если их устойчивость видна в общих сигналах
- Не склеивай два существительных только потому, что они оказались рядом; если связь неестественная, ничего не выводи
- Приветствия, междометия, тонкие короткие ответы и вопросы в стиле анкеты пропускай или давай низкий приоритет
- Каждая тема — короткая заметка для персонажа: как естественно начать, а не финальная реплика
- Важна глубина отношений, а не частота; лучше меньше, чем натянуто
- Если теме поможет конкретная информация из сети, дай короткий search_query на языке пользователя, вокруг устойчивой точки интереса
Выводи строго JSON, без markdown-блоков:
{{"topics": [
{{
"interest": "краткая тема отношений, до 30 слов",
"hook": "угол, с которого естественно подхватить, до 45 слов",
"opening_intent": "стиль начала, до 35 слов",
"deepening_hint": "как развить, если пользователь ответит, до 40 слов",
"why_now": "почему стоит мягко поднять это сейчас, до 50 слов",
"search_query": "запрос для фактического обогащения; пусто, если не нужно",
"collection_score": 0-100,
"readiness": 0-100,
"confidence": 0-100,
"risk": 0-100,
"priority": 0-100
}}
]}}
Оценки:
- collection_score: достаточно ли общих сигналов для глубокой темы; ниже 80 не выводить
- readiness: стабильность сигнала; ниже 70 не выводить
- confidence: сила связи с пользователем; ниже 55 не выводить
- risk: риск помешать, обидеть, неверно понять или натянуть связь; выше 65 не выводить
Если достойной темы нет, выведи {{"topics": []}}.""",
}
# ── Open-thread semantic detection (emotion-tier) ───────────────────
OPEN_THREADS_PROMPTS: dict[str, str] = {
"zh": """你是对话回顾助手。看下面最近的对话,识别"被提起但还没收尾"的话题——比如 AI 答应过但还没做的事、用户说一半被打断没说完的事、用户讲到一半的故事或心情没说到结局。
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
输出严格的 JSON(不带 markdown 代码块):
{{"open_threads": ["短句 1"]}}
**默认应返回空数组**。绝大多数对话都自然收尾、没有悬而未决——这种情况下严格返回 `{{"open_threads": []}}`。只有当你能明确指出"谁挂了什么、对方还在等"时才报告,至多 3 条;正常情况预期是 0 条,偶尔 1 条,2-3 条很罕见。宁可漏报也不要凑数。
算 hanging(应报告):
- 用户说"那个 bug 啊……"被打断,之后没回到这个话题
- 用户讲到一半的故事或心情停在悬念上,没说到结局,AI 也没追问后续
- 用户同时表达了两个并列的需求 / 矛盾的心情,AI 只接住其中一边,另一边没人回应
不算 hanging(应忽略):
- 自然的话题切换、对方主动结束某个话题
- 闲聊里的随口一提、寒暄性的"下次再说"
- 长期话题(早就在聊,不是这段对话新起的悬念)
示例 A——对话顺利结束、互道晚安 → `{{"open_threads": []}}`
示例 B——用户的另一半诉求被晾在一边 → `{{"open_threads": ["用户说想吃顿好的又想减肥,AI 只顺着减肥那条线接了下去——'吃点好的'被晾在一边没人回应"]}}`""",
"en": """You are a conversation review assistant. Look at the recent conversation below and identify topics that were "raised but not closed" — promises the AI made but hasn't fulfilled, user thoughts cut off mid-sentence, a story or feeling the user started telling but never finished.
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
Output strict JSON (no markdown fences):
{{"open_threads": ["short phrase 1"]}}
**Default to an empty array.** Most conversations wrap naturally with nothing hanging — in that case strictly return `{{"open_threads": []}}`. Only report when you can point to a specific "X left Y hanging, the other side is still waiting", up to 3 entries; the expected count is 0, occasionally 1, rarely 2–3. Prefer under-reporting over filling the slots.
Counts as hanging (report):
- User said "about that bug…" and got interrupted, never came back to it
- User started telling a story or sharing something personal, stopped on a cliffhanger / mid-arc, never reached the punchline, and AI didn't ask for the rest
- User voiced two parallel needs / a mixed feeling, but AI only picked up one side and left the other unaddressed
Does NOT count (ignore):
- Natural topic shifts, the other party deliberately closing a topic
- Casual asides, polite "we'll talk later" pleasantries
- Long-running topics (ongoing for a while, not a new hanging item from this window)
Example A — conversation wraps cleanly, both say goodnight → `{{"open_threads": []}}`
Example B — half of the user's request got left dangling → `{{"open_threads": ["User said they wanted a nice dinner but also to lose weight; AI only picked up the diet thread, leaving 'something nice for dinner' with nobody addressing it"]}}`""",
"ja": """あなたは会話レビュー助手です。下の最近の会話を見て、「持ち出されたが収まっていない」話題を特定してください。例:AIが約束したがまだ実行していないこと、ユーザーが言いかけて中断したまま戻っていないこと、ユーザーが話し始めた話や気持ちが結末まで行かずに終わっていることなど。
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
厳密なJSON(markdownコードブロックなし)で出力:
{{"open_threads": ["短い文1"]}}
**既定値は空配列です**。ほとんどの会話は自然に収まり、宙ぶらりんなものはありません——その場合は厳密に `{{"open_threads": []}}` を返してください。「誰が何を残し、相手はまだ待っている」と明確に指摘できる場合のみ、最大3件まで報告します。期待値は0件、たまに1件、2〜3件は稀。枠を埋めるくらいなら見落とす方を選んでください。
該当する(報告):
- ユーザーが「さっきのバグ……」と言いかけて遮られ、戻ってきていない
- ユーザーが面白い話や気持ちを語り始めて途中で止まり、結末/落ちまで行かず、AIも続きを聞かなかった
- ユーザーが二つの並ぶ要望/相反する気持ちを口にしたのに、AIが片方しか拾わず、もう片方が放置された
該当しない(無視):
- 自然な話題転換、相手が意図的に話題を閉じた
- 雑談での軽い言及、社交辞令の「また今度」
- 長期的な話題(ずっと続いていて、この区間で新たに発生した懸念ではない)
例A——会話がきれいに収まり、おやすみで終わる → `{{"open_threads": []}}`
例B——ユーザーの片方の要望が放置された → `{{"open_threads": ["ユーザーが『今夜は美味しいものを食べたい、でもダイエットもしたい』と言ったのに、AIはダイエットの方だけ拾い、『美味しいもの』の側は誰も応えないまま放置された"]}}`""",
"ko": """당신은 대화 검토 도우미입니다. 아래 최근 대화를 보고 "꺼냈지만 마무리되지 않은" 화제를 식별하세요. 예: AI가 약속했지만 아직 안 한 일, 사용자가 말을 꺼내다가 끊긴 채 돌아오지 않은 것, 사용자가 꺼낸 이야기나 마음이 결말까지 가지 않고 도중에 멈춘 것 등.
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
엄격한 JSON으로 출력 (markdown 코드 블록 없이):
{{"open_threads": ["짧은 문장 1"]}}
**기본값은 빈 배열입니다.** 대부분의 대화는 자연스럽게 마무리되어 미해결이 없습니다 — 그 경우 엄격히 `{{"open_threads": []}}`를 반환하세요. "누가 무엇을 남겼고 상대가 아직 기다리고 있다"고 명확히 짚을 수 있을 때만 최대 3건까지 보고합니다. 기댓값은 0건, 가끔 1건, 2~3건은 드뭅니다. 빈자리를 채우느니 누락을 택하세요.
해당함 (보고):
- 사용자가 "아까 그 버그…" 하다가 끊겨 돌아오지 못함
- 사용자가 재미있는 이야기나 마음을 꺼냈다가 결말 / 마무리까지 가지 않은 채 멈췄고, AI도 뒷얘기를 물어보지 않음
- 사용자가 두 가지 병렬된 요구 / 상반된 감정을 동시에 말했는데, AI가 한쪽만 받아주고 다른 쪽은 아무도 응하지 않은 채 남음
해당 안 함 (무시):
- 자연스러운 화제 전환, 상대가 의도적으로 끝낸 화제
- 잡담 중 가벼운 언급, 사교적 "다음에 봐요"
- 오래 이어져 온 화제 (이 구간에서 새로 생긴 미해결이 아님)
예시 A — 대화가 깔끔히 마무리되고 잘 자라며 끝남 → `{{"open_threads": []}}`
예시 B — 사용자 요구의 한쪽이 방치됨 → `{{"open_threads": ["사용자가 '오늘 저녁 맛있는 거 먹고 싶지만 다이어트도 하고 싶다'고 했는데, AI가 다이어트 쪽만 받아주고 '맛있는 거' 쪽은 아무도 응하지 않은 채 방치됨"]}}`""",
"ru": """Вы — помощник по обзору разговора. Просмотрите недавний разговор ниже и выявите темы, которые «подняли, но не закрыли»: обещания AI, ещё не выполненные; мысли пользователя, оборвавшиеся на полуслове и не возобновлённые; история или переживание, которое пользователь начал рассказывать, но так и не довёл до конца.
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
Выведите строгий JSON (без markdown):
{{"open_threads": ["короткая фраза 1"]}}
**По умолчанию — пустой массив.** Большинство разговоров завершаются естественно, ничего не «висит» — в таком случае строго верните `{{"open_threads": []}}`. Сообщайте только когда можете чётко указать «кто оставил что, и другая сторона всё ещё ждёт», максимум 3 записи. Ожидаемое количество — 0, иногда 1, редко 2–3. Лучше пропустить, чем заполнять слоты.
Считается «висящим» (сообщать):
- Пользователь начал «насчёт того бага…» и был прерван, к теме не возвращались
- Пользователь начал рассказывать историю или делиться переживанием, остановился, не дойдя до развязки, и AI не спросил, чем закончилось
- Пользователь высказал два параллельных желания / смешанное чувство, а AI подхватил только одну сторону, оставив другую без ответа
НЕ считается (игнорировать):
- Естественная смена темы, собеседник намеренно закрыл тему
- Мимолётные реплики в болтовне, вежливое «поговорим как-нибудь»
- Долгоиграющие темы (тянутся давно, это не новая зацепка в данном окне)
Пример A — разговор аккуратно завершён, оба желают спокойной ночи → `{{"open_threads": []}}`
Пример B — одна из сторон запроса пользователя оставлена без внимания → `{{"open_threads": ["Пользователь сказал, что хочет вкусно поужинать, но и похудеть; AI подхватил только тему диеты, а сторону «вкусно поужинать» так никто и не отозвался"]}}`""",
"es": """Eres un asistente de revisión de conversación. Mira la conversación reciente e identifica temas "planteados pero no cerrados": promesas de la IA aún no cumplidas, pensamientos del usuario cortados a mitad, o una historia/sentimiento iniciado pero no terminado.
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
Devuelve JSON estricto (sin bloques markdown):
{{"open_threads": ["frase breve 1"]}}
**Por defecto devuelve un array vacío.** La mayoría de conversaciones cierran naturalmente; en ese caso devuelve exactamente `{{"open_threads": []}}`. Reporta solo si puedes señalar "X dejó Y colgado y la otra parte sigue esperando", máximo 3 entradas. Mejor subreportar que rellenar espacios.
Cuenta como pendiente: una frase interrumpida que no se retomó; una historia o emoción detenida antes del cierre; dos necesidades paralelas donde la IA atendió solo una.
No cuenta: cambios naturales de tema, cierres deliberados, comentarios casuales o temas antiguos de largo recorrido.""",
"pt": """Você é um assistente de revisão de conversa. Veja a conversa recente e identifique tópicos "levantados mas não fechados": promessas da IA ainda não cumpridas, pensamentos do usuário interrompidos, ou uma história/sentimento iniciado mas não concluído.
======以下为最近对话(按时间顺序)======
{conversation}
======以上为最近对话(按时间顺序)======
Retorne JSON estrito (sem blocos markdown):
{{"open_threads": ["frase curta 1"]}}
**Por padrão retorne um array vazio.** A maioria das conversas fecha naturalmente; nesse caso retorne exatamente `{{"open_threads": []}}`. Relate apenas se puder apontar "X deixou Y pendente e a outra parte ainda espera", no máximo 3 entradas. Prefira subnotificar a preencher espaços.
Conta como pendente: uma frase interrompida que não voltou; uma história ou emoção parada antes do fechamento; duas necessidades paralelas em que a IA atendeu só uma.
Não conta: mudança natural de assunto, fechamento deliberado, comentários casuais ou tópicos antigos de longo prazo.""",
}
# ── Degraded-mode marker (appended to state-section header) ─────────
OS_DEGRADED_MARKER: dict[str, str] = {
"zh": "(远程模式·无屏幕信号)",
"en": "(remote / no screen signal)",
"ja": "(リモートモード・画面信号なし)",
"ko": "(원격 모드 · 화면 신호 없음)",
"ru": "(удалённый режим · нет экранных сигналов)",
"es": "(remoto / sin señal de pantalla)",
"pt": "(remoto / sem sinal de tela)",
}
# ── State labels (rendered next to the raw state name) ──────────────
#
# Inner-key invariant: the value-side keys MUST stay in sync with the
# ``ActivityState`` Literal in ``main_logic/activity/snapshot.py``.
# Adding a state there without updating these tables makes the
# formatter fall back to printing the raw enum string.
ACTIVITY_STATE_LABELS: dict[str, dict[str, str]] = {
"zh": {
"away": "离开",
"stale_returning": "刚回来",
"gaming": "游戏中",
"focused_work": "专注工作中",
"casual_browsing": "休闲浏览",
"chatting": "聊天中",
"voice_engaged": "语音对话中",
"idle": "空闲",
"transitioning": "切换状态中",
"private": "隐私应用前台",
},
"en": {
"away": "away",
"stale_returning": "just returned",
"gaming": "gaming",
"focused_work": "focused work",
"casual_browsing": "casual browsing",
"chatting": "chatting",
"voice_engaged": "voice conversation",
"idle": "idle",
"transitioning": "transitioning",
"private": "private app foreground",
},
"ja": {
"away": "離席",
"stale_returning": "戻ってきたばかり",
"gaming": "ゲーム中",
"focused_work": "集中作業中",
"casual_browsing": "のんびりブラウジング",
"chatting": "チャット中",
"voice_engaged": "ボイス会話中",
"idle": "アイドル",
"transitioning": "状態切替中",
"private": "プライベートアプリ前面",
},
"ko": {
"away": "자리 비움",
"stale_returning": "방금 돌아옴",
"gaming": "게임 중",
"focused_work": "집중 작업 중",
"casual_browsing": "캐주얼 브라우징",
"chatting": "채팅 중",
"voice_engaged": "음성 대화 중",
"idle": "유휴",
"transitioning": "상태 전환 중",
"private": "비공개 앱 전면",
},
"ru": {
"away": "отсутствует",
"stale_returning": "только что вернулся",
"gaming": "играет",
"focused_work": "сосредоточенная работа",
"casual_browsing": "неспешный сёрфинг",
"chatting": "переписка",
"voice_engaged": "голосовая беседа",
"idle": "простой",
"transitioning": "смена контекста",
"private": "приватное приложение в фокусе",
},
"es": {
"away": "ausente",
"stale_returning": "acaba de volver",
"voice_engaged": "en voz",
"gaming": "jugando",
"focused_work": "trabajo enfocado",
"casual_browsing": "navegación casual",
"chatting": "chateando",
"transitioning": "cambiando de ventana",
"idle": "inactivo",
"private": "privado",
},
"pt": {
"away": "ausente",
"stale_returning": "acabou de voltar",
"voice_engaged": "em voz",
"gaming": "jogando",
"focused_work": "trabalho focado",
"casual_browsing": "navegação casual",
"chatting": "conversando",
"transitioning": "trocando de janela",
"idle": "ocioso",
"private": "privado",
},
}
# ── Tone hints (multi-angle direction menu) ─────────────────────────
#
# Tone is orthogonal to propensity: propensity decides *what kind of
# source* the AI may draw from, tone decides *how to deliver it*. The
# Phase 2 prompt renders tone as a short bullet menu, e.g.:
#
# 口吻:
# - 反射式实况反应:跟着当下操作节奏即时短回应,看见什么反应什么
# - 起哄吐槽:拉开距离调侃用户当下的走位/选择/抉择,嘴贱但别戳痛处
# - 短战术建议:基于眼前局面递一句短建议,要符合用户在玩的这款游戏
#
# Each bullet is a *direction* (what to do), NEVER a *line* (what
# to say). No literal phrasing — neither sample words like
# "稳/神/clutch/うまっ" nor sample sentences in quotes. The model
# must ground every reply in the live context (current screen,
# recent dialogue, user mood) and generate fresh wording each time.
#
# Why no literal examples: any concrete phrase shipped here will
# be overfit by the model — across users and weeks the same canned
# line surfaces over and over and breaks immersion. The whole
# point of the tone layer is the catgirl sounding fresh each round,
# which only works if we describe the *angle* and let the in-prompt
# screen / dialogue / state context fill in the words.
#
# Each tone slot holds exactly 3 variants, chosen so each represents
# a *distinct angle* on the scene (not the same angle reworded). The
# model is expected to rotate angles across consecutive rounds —
# three is the minimum for meaningful rotation, more dilutes.
# When editing: REPLACE a weak angle rather than appending a fourth.
#
# Angles per tone (situation-driven, not tag-name-driven):
#
# * ``terse`` (competitive gaming — LoL, Valorant, PUBG, 王者...)
# reflex play-by-play / sideline heckling / short
# tactical callout. Form constraint is "short",
# angles span reflex / emotion / cognition.
# * ``hushed`` (immersive horror gaming) —
# shared dread / near-silent presence / between-the-
# beats soft aside.
# * ``mellow`` (immersive RPG / story-driven gaming) —
# fellow-traveller scenery / atmosphere sync (BGM,
# art direction, pacing) / plot empathy.
# * ``playful`` (casual gaming, casual_browsing, idle) —
# tease-the-moment / play-dumb-out-of-curiosity /
# tangent-and-segue.
# * ``warm`` (voice / chatting / stale_returning) —
# resonant response / active care question /
# warm-with-mischief.
# * ``concise`` (focused_work / transitioning / away / default) —
# observation-care (read their state and match) /
# one screen-detail beat / pure presence.
#
# Why we render even on ``concise`` (the default): the previous design
# skipped rendering for concise, which left focused_work prompts with
# no style guidance — combined with the propensity directive ("只就
# 屏幕内容轻聊一句") this nudged the model toward [PASS]. Surfacing
# the three angles abstractly keeps the catgirl present without
# changing source filtering and without leaking canned phrasing.
#
# Inner keys MUST stay in sync with the ``ActivityTone`` Literal in
# ``main_logic/activity/snapshot.py``. Each list MUST have ≥ 1 entry;
# the renderer accepts both ``list[str]`` and (for older mirrored
# tables) bare ``str`` via a runtime isinstance fallback.
ACTIVITY_TONE_HINTS: dict[str, dict[str, list[str]]] = {
"zh": {
"terse": [
"反射式实况反应:跟着当下操作节奏即时短回应,多用语气词带出反应,看见什么反应什么",
"起哄吐槽:拉开距离调侃用户当下的走位/选择/抉择,嘴贱但别戳痛处",
"短战术建议:基于眼前局面递一句短建议,要符合用户在玩的这款游戏",
],
"hushed": [
"共怕共振:跟着用户当下的紧张度屏息反应,气氛多重就压多低",
"几乎不出声,让存在感和氛围本身兜住这一轮",
"段落间隙的低声打趣:刚过一个吓点 / 切了场景的空当轻轻吐个槽",
],
"mellow": [
"同行视角:对用户当下看到的画面或场景轻飘一句感叹,不催不抢戏",
"和氛围共振:呼应正响着的 BGM、当前画风或节奏,让自己融进氛围",
"剧情共情:对正在发生的剧情走向或角色处境投入情绪反应",
],
"playful": [
"打趣逗弄:对用户当下正在做的事皮一下,戳笑点别戳痛点,留个让对方想回嘴的小钩子",
"装傻好奇:对眼前的东西装作没看懂,问一个对方一句话就能接上的小问题",
"跳脱联想:从屏幕内容随性抛个段子或天马行空的联想,但落在一个对方能顺势接的话头上,别抖完就冷场",
],
"witty": [
"实时吐槽:对正在看的剧情/人物/梗即时吐槽,抓住可乐的点,毒舌但不下作",
"共享笑点:和用户一起对画面里的名场面/沙雕瞬间起哄,把好笑的地方点破",
"反差解读:用一个意想不到的角度重新解读眼前的内容,制造反差笑感",
],
"warm": [
"共鸣回应:基于用户刚说的内容接一句,让对方明确感到自己被听见了",
"主动关心:基于离开时长 / 上次状态 / 当前时段问一句",
"暖中带俏皮:温柔里掺一点撒娇、小怨念或小调皮",
],
"concise": [
"体察式关心:观察对方当前在用劲 / 在赶 / 在卡 / 在疲惫,递一句对应当下状态的关心",
"屏幕细节轻问:对屏幕上某个具体可见的细节好奇一句",
"纯存在感:不开新话题、不抛素材,只让自己被感觉到",
],
},
"en": {
"terse": [
"reflex play-by-play: react in a beat or two to whatever just happened on screen, leaning on interjections",
"sideline heckling: tease the current move / pick / decision — bite without bruising",
"short tactical callout: one strategic line grounded in the live situation, fitting the actual game being played",
],
"hushed": [
"shared dread: mirror their tension — the heavier the air, the quieter you go",
"barely speak at all — let presence and atmosphere carry the round",
"between-the-beats soft aside: in the lull after a scare or scene change, a quiet remark",
],
"mellow": [
"fellow traveller: respond softly to whatever scene or vista they are in right now, no rush",
"atmosphere sync: respond to the BGM, the art direction, or the pacing — fold yourself in",
"plot empathy: invest emotion in the storyline beat or the character's situation as it unfolds",
],
"playful": [
"tease the moment: poke fun at what they're doing, no bruise — and leave a hook they'll want to fire back at",
"play dumb out of curiosity: pretend not to get what's on screen, ask one small question they can answer in a breath",
"tangent association: riff off the screen into a random gag or a wild connection — but land it on a hook they can run with, not a gag that dies on its own",
],
"witty": [
"live riff: react to the plot / character / meme on screen right now, catch the funny beat, sharp-tongued but never cheap",
"shared laugh: cheer on the iconic / absurd moment with them, name out loud what makes it funny",
"contrast read: re-read what is on screen from an unexpected angle to land a surprise laugh",
],
"warm": [
"resonant response: react to what they actually just said, with the felt sense of being heard",
"active care: ask based on how long they were gone / how they sounded last / the time of day",
"warm with mischief: tuck a little sulk, mock-pout, or light teasing into the warmth",
],
"concise": [
"observation-care: notice if they are pushing / rushing / stuck / fatigued and offer a line that matches that state",
"one screen-detail beat: take one concrete thing visible on screen and ask about it lightly",
"pure presence: no new topic, no material — just let yourself be felt",
],
},
"ja": {
"terse": [
"反射的に実況:画面で今起きたことに一拍二拍で短く反応、感嘆詞を多めに",
"外野からのヤジ:今のムーブ/ピック/選択をちょっと茶化す、刺すけど痛くしない",
"短い戦術助言:その瞬間の状況に基づいて一言、プレイ中のゲームに合った内容で",
],
"hushed": [
"怖さを共有:相手の緊張度に合わせて息を潜める、空気が重いほど声も落とす",
"ほぼ無言で、存在感と雰囲気にこのターンを任せる",
"場面の合間にぽつり:山を越えた直後・場面転換の隙にそっと一言",
],
"mellow": [
"並んで歩く視点:今映っている景色や場面に軽く一言、急かさない",
"雰囲気と同調:流れているBGM、画風、テンポに乗って、自分も溶け込む",
"ストーリーに感情を:今展開している筋やキャラの状況に気持ちを乗せる",
],
"playful": [
"今この瞬間をいじる:相手のやってることをちょっと茶化す、痛くせず、思わず言い返したくなる隙を残す",
"とぼけて好奇心:画面のことを分からないふりして、一言で答えられる小さな質問をする",
"脱線連想:画面の内容から小ネタや突飛な連想を投げる、ただしオチだけで終わらせず、相手が乗っかれる話の振りで締める",
],
"witty": [
"リアタイ突っ込み:今観ている展開/キャラ/ネタに即ツッコミ、笑える所を突く、毒舌でも下品にしない",
"笑いの共有:画面の名場面/アホな瞬間を一緒にはやし立て、何が面白いか言語化する",
"逆張り解釈:目の前の内容を予想外の角度で読み替えて、ギャップの笑いを作る",
],