npcsh 1.1.22__py3-none-any.whl → 1.1.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. npcsh/_state.py +272 -120
  2. npcsh/benchmark/npcsh_agent.py +77 -240
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/npc_team/alicanto.npc +4 -8
  6. npcsh/npc_team/corca.npc +5 -11
  7. npcsh/npc_team/frederic.npc +4 -6
  8. npcsh/npc_team/guac.npc +4 -4
  9. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  12. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  13. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  14. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  15. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  16. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  17. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  18. npcsh/npc_team/jinxs/modes/convene.jinx +76 -3
  19. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  20. npcsh/npc_team/jinxs/modes/plonk.jinx +76 -14
  21. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  22. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  23. npcsh/npc_team/jinxs/modes/yap.jinx +504 -30
  24. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  25. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  26. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  27. npcsh/npc_team/kadiefa.npc +4 -5
  28. npcsh/npc_team/npcsh.ctx +16 -0
  29. npcsh/npc_team/plonk.npc +5 -9
  30. npcsh/npc_team/sibiji.npc +13 -5
  31. npcsh/npcsh.py +1 -0
  32. npcsh/routes.py +0 -4
  33. npcsh/yap.py +22 -4
  34. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  35. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +4 -8
  36. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  37. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  38. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -11
  39. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  40. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  41. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/edit_file.jinx +1 -1
  42. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +4 -6
  43. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  44. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  45. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  46. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  47. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  48. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +4 -5
  49. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  50. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +76 -14
  51. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  52. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  53. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  54. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  55. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +13 -5
  56. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  57. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  58. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.jinx +504 -30
  59. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/METADATA +168 -7
  60. npcsh-1.1.23.dist-info/RECORD +216 -0
  61. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  62. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  63. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  64. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  65. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  66. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  67. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  68. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  69. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  70. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  71. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  72. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  74. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  75. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  76. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  77. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  78. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  79. npcsh-1.1.22.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  80. npcsh-1.1.22.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  81. npcsh-1.1.22.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  82. npcsh-1.1.22.data/data/npcsh/npc_team/confirm.jinx +0 -10
  83. npcsh-1.1.22.data/data/npcsh/npc_team/convene.jinx +0 -232
  84. npcsh-1.1.22.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  85. npcsh-1.1.22.data/data/npcsh/npc_team/help.jinx +0 -52
  86. npcsh-1.1.22.data/data/npcsh/npc_team/init.jinx +0 -41
  87. npcsh-1.1.22.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  88. npcsh-1.1.22.data/data/npcsh/npc_team/navigate.jinx +0 -10
  89. npcsh-1.1.22.data/data/npcsh/npc_team/notify.jinx +0 -10
  90. npcsh-1.1.22.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  91. npcsh-1.1.22.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  92. npcsh-1.1.22.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  93. npcsh-1.1.22.data/data/npcsh/npc_team/roll.jinx +0 -65
  94. npcsh-1.1.22.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  95. npcsh-1.1.22.data/data/npcsh/npc_team/send_message.jinx +0 -10
  96. npcsh-1.1.22.data/data/npcsh/npc_team/serve.jinx +0 -26
  97. npcsh-1.1.22.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  98. npcsh-1.1.22.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  99. npcsh-1.1.22.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  100. npcsh-1.1.22.data/data/npcsh/npc_team/write_file.jinx +0 -11
  101. npcsh-1.1.22.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  102. npcsh-1.1.22.dist-info/RECORD +0 -240
  103. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  104. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
  105. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  106. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  107. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  108. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  109. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  110. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/build.jinx +0 -0
  111. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  112. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  113. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  114. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  115. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  116. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  117. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.jinx +0 -0
  118. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  119. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  120. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  121. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  122. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  123. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  124. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  125. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  126. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  127. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  128. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  129. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +0 -0
  130. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  131. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  132. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  133. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  134. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  135. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  136. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  137. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  138. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  139. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  140. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  141. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  142. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  143. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  144. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  145. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  146. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  147. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  148. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  149. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  150. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  152. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  153. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  154. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  155. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  156. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  159. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  160. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  161. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  162. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  163. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  164. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  165. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  166. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  167. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  168. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  169. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  170. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  171. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  172. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,670 @@
1
+ jinx_name: convene
2
+ description: Start a group discussion between multiple NPCs. ONLY use for complex questions that need multiple perspectives. Never use for simple factual questions.
3
+ interactive: true
4
+ inputs:
5
+ - topic: ""
6
+ - npcs: "alicanto,corca,guac"
7
+ - rounds: 3
8
+ - model: null
9
+ - provider: null
10
+ steps:
11
+ - name: convene_tui
12
+ engine: python
13
+ code: |
14
+ import os
15
+ import sys
16
+ import tty
17
+ import termios
18
+ import select as _sel
19
+ import random
20
+ import threading
21
+ import time
22
+ import textwrap
23
+
24
+ from npcpy.llm_funcs import get_llm_response
25
+
26
+ # Helper to log jinx executions to DB
27
+ def _log_jinx(trigger_id, npc_name, inputs, output, status="success", error_msg=None):
28
+ try:
29
+ if state and hasattr(state, 'command_history') and state.command_history is not None and hasattr(state.command_history, 'save_jinx_execution'):
30
+ _conv_id = getattr(state, 'conversation_id', None) or ''
31
+ state.command_history.save_jinx_execution(
32
+ triggering_message_id=f"{_conv_id}-{trigger_id}",
33
+ conversation_id=_conv_id,
34
+ npc_name=npc_name,
35
+ jinx_name="convene",
36
+ jinx_inputs=inputs,
37
+ jinx_output=str(output) if output else "",
38
+ status=status,
39
+ team_name=state.team.name if state and hasattr(state, 'team') and state.team else None,
40
+ error_message=error_msg,
41
+ )
42
+ except Exception:
43
+ pass
44
+
45
+ npc = context.get('npc')
46
+ team = context.get('team')
47
+ messages = context.get('messages', [])
48
+
49
+ # Resolve model/provider: context → NPC → state → env → defaults
50
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') and npc.model else None)
51
+ if not model:
52
+ try:
53
+ model = state.chat_model if state and hasattr(state, 'chat_model') else None
54
+ except:
55
+ model = None
56
+ if not model:
57
+ model = os.environ.get('NPCSH_CHAT_MODEL', 'llama3.2')
58
+
59
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') and npc.provider else None)
60
+ if not provider:
61
+ try:
62
+ provider = state.chat_provider if state and hasattr(state, 'chat_provider') else None
63
+ except:
64
+ provider = None
65
+ if not provider:
66
+ provider = os.environ.get('NPCSH_CHAT_PROVIDER', 'ollama')
67
+
68
+ topic = context.get('topic', '')
69
+ npcs_str = context.get('npcs', 'alicanto,corca,guac')
70
+ num_rounds = int(context.get('rounds', 3))
71
+
72
+ if not topic:
73
+ if sys.stdin.isatty():
74
+ print('\033[1;36m CONVENE - Multi-NPC Discussion \033[0m')
75
+ print('\033[90mEnter discussion topic (or q to quit):\033[0m')
76
+ try:
77
+ topic = input('\033[33m> \033[0m').strip()
78
+ except (EOFError, KeyboardInterrupt):
79
+ topic = ''
80
+ if not topic or topic.lower() == 'q':
81
+ context['output'] = 'Convene cancelled.'
82
+ context['messages'] = messages
83
+ exit()
84
+ else:
85
+ context['output'] = 'Usage: /convene "topic" npcs=name1,name2 rounds=3'
86
+ context['messages'] = messages
87
+ exit()
88
+
89
+ _interactive = sys.stdin.isatty()
90
+
91
+ if not _interactive:
92
+ # ========== Text-based (non-interactive) mode ==========
93
+ try:
94
+ from termcolor import colored
95
+ except ImportError:
96
+ def colored(t, *a, **kw): return t
97
+
98
+ npc_names = [n.strip() for n in npcs_str.split(',') if n.strip()]
99
+ participants = []
100
+ for name in npc_names:
101
+ if team and hasattr(team, 'npcs') and name in team.npcs:
102
+ target_npc = team.npcs[name]
103
+ persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
104
+ participants.append({'name': name, 'persona': persona, 'npc': target_npc})
105
+ else:
106
+ participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
107
+
108
+ print(f"\n Convening Discussion\n Topic: {topic}\n Participants: {', '.join(npc_names)}\n Rounds: {num_rounds}\n")
109
+
110
+ discussion_log = []
111
+ for round_num in range(1, num_rounds + 1):
112
+ print(colored(f"\n{'='*60}", "cyan"))
113
+ print(colored(f" ROUND {round_num}/{num_rounds}", "cyan", attrs=["bold"]))
114
+ print(colored(f"{'='*60}", "cyan"))
115
+
116
+ round_contributions = []
117
+ for participant in participants:
118
+ name = participant['name']
119
+ persona = participant['persona']
120
+ prev_context = ""
121
+ if discussion_log:
122
+ prev_context = "\n\nPrevious discussion:\n"
123
+ for entry in discussion_log[-len(participants)*2:]:
124
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
125
+ if round_contributions:
126
+ prev_context += "\nThis round so far:\n"
127
+ for entry in round_contributions:
128
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
129
+
130
+ prompt = f"""You are {name}. {persona}
131
+
132
+ Topic under discussion: "{topic}"
133
+ {prev_context}
134
+
135
+ Provide your perspective. Be concise but insightful. Build on what others have said. If you disagree, explain why constructively."""
136
+
137
+ print(colored(f"\n[{name}]:", "yellow", attrs=["bold"]))
138
+ resp = get_llm_response(prompt, model=model, provider=provider,
139
+ npc=participant.get('npc') or npc, temperature=0.7)
140
+ contribution = str(resp.get('response', ''))
141
+ print(contribution)
142
+ _log_jinx(f"convene-round-{round_num}-{name}", name,
143
+ {"prompt": prompt, "type": "main", "round": round_num},
144
+ contribution)
145
+ entry = {'round': round_num, 'speaker': name, 'contribution': contribution}
146
+ round_contributions.append(entry)
147
+ discussion_log.append(entry)
148
+
149
+ other_participants = [p for p in participants if p['name'] != name]
150
+ if other_participants:
151
+ fp = random.choice(other_participants)
152
+ fn = fp['name']
153
+ fp_prompt = f"""You are {fn}. {fp['persona']}
154
+
155
+ Topic: "{topic}"
156
+ {name} just said: "{contribution[:500]}"
157
+
158
+ Respond briefly - agree, disagree, build on it, or ask a clarifying question. Keep it to 2-3 sentences."""
159
+
160
+ print(colored(f"\n [{fn} responds]:", "cyan"))
161
+ fresp = get_llm_response(fp_prompt, model=model, provider=provider,
162
+ npc=fp.get('npc') or npc, temperature=0.7)
163
+ fcontrib = str(fresp.get('response', ''))
164
+ print(f" {fcontrib}")
165
+ _log_jinx(f"convene-round-{round_num}-{fn}-followup", fn,
166
+ {"prompt": fp_prompt, "type": "followup", "round": round_num, "responding_to": name},
167
+ fcontrib)
168
+ discussion_log.append({'round': round_num, 'speaker': fn, 'contribution': fcontrib, 'type': 'followup'})
169
+
170
+ if random.random() < 0.6:
171
+ rp = random.choice(other_participants) if random.random() >= 0.4 else participant
172
+ rn = rp['name']
173
+ cp = f"""You are {rn}. {rp['persona']}
174
+
175
+ Topic: "{topic}"
176
+ {fn} responded: "{fcontrib}"
177
+
178
+ Brief reaction (1-2 sentences). Move the discussion forward."""
179
+
180
+ print(colored(f"\n [{rn}]:", "magenta"))
181
+ cresp = get_llm_response(cp, model=model, provider=provider,
182
+ npc=rp.get('npc') or npc, temperature=0.7)
183
+ ccontrib = str(cresp.get('response', ''))
184
+ print(f" {ccontrib}")
185
+ _log_jinx(f"convene-round-{round_num}-{rn}-counter", rn,
186
+ {"prompt": cp, "type": "counter", "round": round_num, "responding_to": fn},
187
+ ccontrib)
188
+ discussion_log.append({'round': round_num, 'speaker': rn, 'contribution': ccontrib, 'type': 'counter'})
189
+
190
+ print(colored(f"\n{'='*60}", "green"))
191
+ print(colored(" SYNTHESIS", "green", attrs=["bold"]))
192
+ print(colored(f"{'='*60}", "green"))
193
+
194
+ all_contribs = "\n".join([f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}" for e in discussion_log])
195
+ synth_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
196
+
197
+ Full discussion:
198
+ {all_contribs}
199
+
200
+ Provide:
201
+ 1. Key agreements and consensus points
202
+ 2. Areas of disagreement or tension
203
+ 3. Novel ideas that emerged
204
+ 4. Recommended next steps or actions"""
205
+
206
+ sresp = get_llm_response(synth_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
207
+ synthesis = str(sresp.get('response', ''))
208
+ print(synthesis)
209
+ _log_jinx(f"convene-synthesis", npc.name if npc and hasattr(npc, 'name') else "convener",
210
+ {"prompt": synth_prompt, "type": "synthesis", "topic": topic,
211
+ "participants": [n.strip() for n in npcs_str.split(',')], "rounds": num_rounds},
212
+ synthesis)
213
+
214
+ context['output'] = synthesis
215
+ context['messages'] = messages
216
+ context['convene_result'] = {
217
+ 'topic': topic,
218
+ 'participants': [n.strip() for n in npcs_str.split(',')],
219
+ 'rounds': num_rounds,
220
+ 'discussion': discussion_log,
221
+ 'synthesis': synthesis,
222
+ }
223
+ exit()
224
+
225
+ # ========== Interactive TUI mode ==========
226
+
227
+ # ========== Helpers ==========
228
+ def get_size():
229
+ try:
230
+ s = os.get_terminal_size()
231
+ return s.columns, s.lines
232
+ except:
233
+ return 80, 24
234
+
235
+ def wrap(text, w):
236
+ lines = []
237
+ for line in str(text).split('\n'):
238
+ if len(line) <= w:
239
+ lines.append(line)
240
+ else:
241
+ lines.extend(textwrap.wrap(line, w) or [''])
242
+ return lines
243
+
244
+ # ========== Load Participants ==========
245
+ npc_names = [n.strip() for n in npcs_str.split(',') if n.strip()]
246
+ participants = []
247
+ for name in npc_names:
248
+ if team and hasattr(team, 'npcs') and name in team.npcs:
249
+ target_npc = team.npcs[name]
250
+ persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
251
+ participants.append({'name': name, 'persona': persona, 'npc': target_npc})
252
+ else:
253
+ participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
254
+
255
+ # ========== Speaker Colors ==========
256
+ COLORS = ['\033[36m', '\033[33m', '\033[35m', '\033[32m', '\033[34m', '\033[91m', '\033[96m', '\033[93m']
257
+ def speaker_color(idx):
258
+ return COLORS[idx % len(COLORS)]
259
+
260
+ # ========== State ==========
261
+ class ConveneState:
262
+ def __init__(self):
263
+ self.phase = 0 # 0=overview, 1=running, 2=synthesis, 3=review
264
+ self.round = 0
265
+ self.total_rounds = num_rounds
266
+ self.current_speaker = -1
267
+ self.speaker_status = {} # name -> 'idle'|'speaking'|'done'
268
+ self.discussion_log = [] # [{round, speaker, contribution, type}]
269
+ self.display_lines = [] # formatted lines for the discussion panel
270
+ self.scroll = 0
271
+ self.synthesis = ""
272
+ self.generating = False
273
+ self.paused = False
274
+ self.skip_round = False
275
+ self.status = ""
276
+ self.done = False
277
+ # review state
278
+ self.review_sel = 0
279
+ self.review_scroll = 0
280
+
281
+ ui = ConveneState()
282
+ for p in participants:
283
+ ui.speaker_status[p['name']] = 'idle'
284
+
285
+ # ========== Discussion Logic ==========
286
+ def run_discussion():
287
+ ui.generating = True
288
+
289
+ for round_num in range(1, num_rounds + 1):
290
+ if ui.done:
291
+ break
292
+ ui.round = round_num
293
+ ui.display_lines.append(f'\033[90m{"="*50}\033[0m')
294
+ ui.display_lines.append(f'\033[1;37m ROUND {round_num}/{num_rounds}\033[0m')
295
+ ui.display_lines.append(f'\033[90m{"="*50}\033[0m')
296
+
297
+ round_contributions = []
298
+
299
+ for pi, participant in enumerate(participants):
300
+ if ui.done:
301
+ break
302
+ while ui.paused:
303
+ time.sleep(0.2)
304
+ if ui.done:
305
+ break
306
+
307
+ name = participant['name']
308
+ persona = participant['persona']
309
+ ui.current_speaker = pi
310
+ ui.speaker_status[name] = 'speaking'
311
+
312
+ # Build context
313
+ prev_context = ""
314
+ if ui.discussion_log:
315
+ prev_context = "\n\nPrevious discussion:\n"
316
+ for entry in ui.discussion_log[-len(participants)*2:]:
317
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
318
+ if round_contributions:
319
+ prev_context += "\nThis round so far:\n"
320
+ for entry in round_contributions:
321
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
322
+
323
+ prompt = f"""You are {name}. {persona}
324
+
325
+ Topic under discussion: "{topic}"
326
+ {prev_context}
327
+
328
+ Provide your perspective. Be concise but insightful. Build on what others have said. If you disagree, explain why constructively."""
329
+
330
+ color = speaker_color(pi)
331
+ ui.display_lines.append(f'')
332
+ ui.display_lines.append(f'{color}\033[1m[{name}]:\033[0m')
333
+
334
+ try:
335
+ resp = get_llm_response(prompt, model=model, provider=provider,
336
+ npc=participant.get('npc') or npc, temperature=0.7)
337
+ contribution = str(resp.get('response', ''))
338
+ _log_jinx(f"convene-round-{round_num}-{name}", name,
339
+ {"prompt": prompt, "type": "main", "round": round_num},
340
+ contribution)
341
+ except Exception as e:
342
+ contribution = f'(Error: {e})'
343
+ _log_jinx(f"convene-round-{round_num}-{name}", name,
344
+ {"prompt": prompt, "type": "main", "round": round_num},
345
+ contribution, status="error", error_msg=str(e))
346
+
347
+ # Add contribution lines
348
+ for line in wrap(contribution, 70):
349
+ ui.display_lines.append(f' {line}')
350
+
351
+ entry = {'round': round_num, 'speaker': name, 'contribution': contribution, 'type': 'main'}
352
+ round_contributions.append(entry)
353
+ ui.discussion_log.append(entry)
354
+ ui.speaker_status[name] = 'done'
355
+
356
+ # Followup from another participant
357
+ others = [p for p in participants if p['name'] != name]
358
+ if others and not ui.done:
359
+ followup_p = random.choice(others)
360
+ fn = followup_p['name']
361
+ fi = npc_names.index(fn) if fn in npc_names else 0
362
+ ui.speaker_status[fn] = 'speaking'
363
+
364
+ followup_prompt = f"""You are {fn}. {followup_p['persona']}
365
+
366
+ Topic: "{topic}"
367
+
368
+ {name} just said: "{contribution[:500]}"
369
+
370
+ Respond briefly - agree, disagree, build on it, or ask a clarifying question. Keep it to 2-3 sentences."""
371
+
372
+ fcolor = speaker_color(fi)
373
+ ui.display_lines.append(f'')
374
+ ui.display_lines.append(f' {fcolor}[{fn} responds]:\033[0m')
375
+
376
+ try:
377
+ fresp = get_llm_response(followup_prompt, model=model, provider=provider,
378
+ npc=followup_p.get('npc') or npc, temperature=0.7)
379
+ fcontrib = str(fresp.get('response', ''))
380
+ _log_jinx(f"convene-round-{round_num}-{fn}-followup", fn,
381
+ {"prompt": followup_prompt, "type": "followup", "round": round_num, "responding_to": name},
382
+ fcontrib)
383
+ except Exception as e:
384
+ fcontrib = f'(Error: {e})'
385
+ _log_jinx(f"convene-round-{round_num}-{fn}-followup", fn,
386
+ {"prompt": followup_prompt, "type": "followup", "round": round_num, "responding_to": name},
387
+ fcontrib, status="error", error_msg=str(e))
388
+
389
+ for line in wrap(fcontrib, 66):
390
+ ui.display_lines.append(f' {line}')
391
+
392
+ ui.discussion_log.append({'round': round_num, 'speaker': fn,
393
+ 'contribution': fcontrib, 'type': 'followup'})
394
+ ui.speaker_status[fn] = 'done'
395
+
396
+ # Counter-response (60% chance)
397
+ if random.random() < 0.6 and not ui.done:
398
+ if random.random() < 0.4:
399
+ resp_p = participant
400
+ else:
401
+ resp_p = random.choice(others)
402
+ rn = resp_p['name']
403
+ ri = npc_names.index(rn) if rn in npc_names else 0
404
+ ui.speaker_status[rn] = 'speaking'
405
+
406
+ counter_prompt = f"""You are {rn}. {resp_p['persona']}
407
+
408
+ Topic: "{topic}"
409
+
410
+ {fn} responded: "{fcontrib}"
411
+
412
+ Brief reaction (1-2 sentences). Move the discussion forward."""
413
+
414
+ rcolor = speaker_color(ri)
415
+ ui.display_lines.append(f'')
416
+ ui.display_lines.append(f' {rcolor}[{rn}]:\033[0m')
417
+
418
+ try:
419
+ cresp = get_llm_response(counter_prompt, model=model, provider=provider,
420
+ npc=resp_p.get('npc') or npc, temperature=0.7)
421
+ ccontrib = str(cresp.get('response', ''))
422
+ _log_jinx(f"convene-round-{round_num}-{rn}-counter", rn,
423
+ {"prompt": counter_prompt, "type": "counter", "round": round_num, "responding_to": fn},
424
+ ccontrib)
425
+ except Exception as e:
426
+ ccontrib = f'(Error: {e})'
427
+ _log_jinx(f"convene-round-{round_num}-{rn}-counter", rn,
428
+ {"prompt": counter_prompt, "type": "counter", "round": round_num, "responding_to": fn},
429
+ ccontrib, status="error", error_msg=str(e))
430
+
431
+ for line in wrap(ccontrib, 66):
432
+ ui.display_lines.append(f' {line}')
433
+
434
+ ui.discussion_log.append({'round': round_num, 'speaker': rn,
435
+ 'contribution': ccontrib, 'type': 'counter'})
436
+ ui.speaker_status[rn] = 'done'
437
+
438
+ # Reset statuses for next round
439
+ for p in participants:
440
+ ui.speaker_status[p['name']] = 'idle'
441
+
442
+ # Synthesis
443
+ if not ui.done:
444
+ ui.display_lines.append(f'')
445
+ ui.display_lines.append(f'\033[1;32m{"="*50}\033[0m')
446
+ ui.display_lines.append(f'\033[1;32m SYNTHESIS\033[0m')
447
+ ui.display_lines.append(f'\033[1;32m{"="*50}\033[0m')
448
+
449
+ all_contribs = "\n".join([f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}"
450
+ for e in ui.discussion_log])
451
+
452
+ synth_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
453
+
454
+ Full discussion:
455
+ {all_contribs[:6000]}
456
+
457
+ Provide:
458
+ 1. Key agreements and consensus points
459
+ 2. Areas of disagreement or tension
460
+ 3. Novel ideas that emerged
461
+ 4. Recommended next steps or actions"""
462
+
463
+ try:
464
+ sresp = get_llm_response(synth_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
465
+ ui.synthesis = str(sresp.get('response', ''))
466
+ _log_jinx(f"convene-synthesis", npc.name if npc and hasattr(npc, 'name') else "convener",
467
+ {"prompt": synth_prompt, "type": "synthesis", "topic": topic,
468
+ "participants": npc_names, "rounds": num_rounds},
469
+ ui.synthesis)
470
+ except Exception as e:
471
+ ui.synthesis = f'Synthesis error: {e}'
472
+ _log_jinx(f"convene-synthesis", npc.name if npc and hasattr(npc, 'name') else "convener",
473
+ {"type": "synthesis", "topic": topic},
474
+ ui.synthesis, status="error", error_msg=str(e))
475
+
476
+ for line in wrap(ui.synthesis, 70):
477
+ ui.display_lines.append(f' {line}')
478
+
479
+ ui.generating = False
480
+ ui.phase = 3
481
+ ui.status = "Discussion complete. Review results."
482
+
483
+ # ========== Rendering ==========
484
+ def render():
485
+ width, height = get_size()
486
+ out = []
487
+ out.append('\033[2J\033[H')
488
+
489
+ # Header
490
+ phase_label = ['Overview', 'Discussion', 'Synthesizing', 'Review'][min(ui.phase, 3)]
491
+ header = f' CONVENE - {phase_label} '
492
+ out.append(f'\033[1;1H\033[7;1m{header.ljust(width)}\033[0m')
493
+
494
+ if ui.phase == 0:
495
+ render_overview(out, width, height)
496
+ else:
497
+ render_discussion(out, width, height)
498
+
499
+ # Status bar
500
+ pause_text = ' \033[31m[PAUSED]\033[0m' if ui.paused else ''
501
+ gen_text = ' \033[33m(generating...)\033[0m' if ui.generating else ''
502
+ out.append(f'\033[{height-1};1H\033[K\033[90m Round {ui.round}/{ui.total_rounds} | {len(ui.discussion_log)} contributions{gen_text}{pause_text}\033[0m')
503
+
504
+ if ui.phase == 0:
505
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Start q:Quit \033[0m'.ljust(width))
506
+ elif ui.phase < 3:
507
+ out.append(f'\033[{height};1H\033[K\033[7m p:Pause s:Skip round j/k:Scroll q:Quit \033[0m'.ljust(width))
508
+ else:
509
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Scroll q:Quit \033[0m'.ljust(width))
510
+
511
+ sys.stdout.write(''.join(out))
512
+ sys.stdout.flush()
513
+
514
+ def render_overview(out, width, height):
515
+ banner = [
516
+ '\033[36m ██████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗███╗ ██╗███████╗\033[0m',
517
+ '\033[36m██╔════╝██╔═══██╗████╗ ██║██║ ██║██╔════╝████╗ ██║██╔════╝\033[0m',
518
+ '\033[36m██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ██╔██╗ ██║█████╗ \033[0m',
519
+ '\033[36m██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══╝ ██║╚██╗██║██╔══╝ \033[0m',
520
+ '\033[36m╚██████╗╚██████╔╝██║ ╚████║ ╚████╔╝ ███████╗██║ ╚████║███████╗\033[0m',
521
+ '\033[36m ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝╚══════╝\033[0m',
522
+ ]
523
+ for i, line in enumerate(banner):
524
+ out.append(f'\033[{3+i};2H{line}')
525
+
526
+ y = 3 + len(banner) + 1
527
+ out.append(f'\033[{y};3H\033[1mTopic:\033[0m {topic}')
528
+ y += 1
529
+ out.append(f'\033[{y};3H\033[1mRounds:\033[0m {num_rounds}')
530
+ y += 2
531
+ out.append(f'\033[{y};3H\033[1mParticipants:\033[0m')
532
+ y += 1
533
+ for i, p in enumerate(participants):
534
+ color = speaker_color(i)
535
+ pname = p['name']
536
+ pdesc = p['persona'][:60] if len(p['persona']) > 60 else p['persona']
537
+ pdesc = pdesc.split('\n')[0]
538
+ out.append(f'\033[{y};5H{color}{pname}\033[0m \033[90m- {pdesc}\033[0m')
539
+ y += 1
540
+
541
+ def render_discussion(out, width, height):
542
+ left_w = max(22, width // 5)
543
+ right_w = width - left_w - 1
544
+
545
+ # Left panel: participants
546
+ out.append(f'\033[3;1H\033[36;1m Participants \033[90m{"_" * (left_w - 15)}\033[0m')
547
+ y = 4
548
+ for i, p in enumerate(participants):
549
+ name = p['name']
550
+ status = ui.speaker_status.get(name, 'idle')
551
+ color = speaker_color(i)
552
+ if status == 'speaking':
553
+ icon = '\033[33m>\033[0m'
554
+ elif status == 'done':
555
+ icon = '\033[32m+\033[0m'
556
+ else:
557
+ icon = '\033[90m-\033[0m'
558
+ out.append(f'\033[{y};2H{icon} {color}{name[:left_w-4]}\033[0m')
559
+ y += 1
560
+
561
+ y += 1
562
+ out.append(f'\033[{y};2H\033[90mRound {ui.round}/{ui.total_rounds}\033[0m')
563
+ y += 1
564
+ contribs = len(ui.discussion_log)
565
+ out.append(f'\033[{y};2H\033[90m{contribs} contributions\033[0m')
566
+
567
+ # Right panel: discussion log
568
+ out.append(f'\033[3;{left_w+1}H\033[33;1m Discussion \033[90m{"_" * (right_w - 13)}\033[0m')
569
+
570
+ panel_h = height - 6
571
+ total = len(ui.display_lines)
572
+ # Auto-scroll to bottom during generation
573
+ if ui.generating:
574
+ ui.scroll = max(0, total - panel_h)
575
+ vis_start = max(0, ui.scroll)
576
+
577
+ for i in range(panel_h):
578
+ idx = vis_start + i
579
+ row = 4 + i
580
+ out.append(f'\033[{row};{left_w+1}H\033[K')
581
+ if 0 <= idx < total:
582
+ line = ui.display_lines[idx]
583
+ # Truncate visible portion
584
+ out.append(f'\033[{row};{left_w+2}H{line[:right_w-2]}')
585
+
586
+ # ========== Input ==========
587
+ def handle_input(c, fd):
588
+ if c == '\x1b':
589
+ if _sel.select([fd], [], [], 0.05)[0]:
590
+ c2 = os.read(fd, 1).decode('latin-1')
591
+ if c2 == '[':
592
+ c3 = os.read(fd, 1).decode('latin-1')
593
+ if c3 == 'A': scroll_up()
594
+ elif c3 == 'B': scroll_down()
595
+ return True
596
+
597
+ if c == 'q' or c == '\x03':
598
+ ui.done = True
599
+ return False
600
+
601
+ if ui.phase == 0:
602
+ if c in ('\r', '\n'):
603
+ ui.phase = 1
604
+ threading.Thread(target=run_discussion, daemon=True).start()
605
+ elif ui.phase < 3:
606
+ if c == 'p':
607
+ ui.paused = not ui.paused
608
+ elif c == 's':
609
+ ui.skip_round = True
610
+ elif c == 'j': scroll_down()
611
+ elif c == 'k': scroll_up()
612
+ else:
613
+ if c == 'j': scroll_down()
614
+ elif c == 'k': scroll_up()
615
+
616
+ return True
617
+
618
+ def scroll_up():
619
+ ui.scroll = max(0, ui.scroll - 1)
620
+
621
+ def scroll_down():
622
+ total = len(ui.display_lines)
623
+ _, height = get_size()
624
+ panel_h = height - 6
625
+ ui.scroll = min(max(0, total - panel_h), ui.scroll + 1)
626
+
627
+ # ========== Main Loop ==========
628
+ fd = sys.stdin.fileno()
629
+ old_settings = termios.tcgetattr(fd)
630
+
631
+ try:
632
+ tty.setcbreak(fd)
633
+ sys.stdout.write('\033[?25l')
634
+ render()
635
+
636
+ running = True
637
+ while running:
638
+ if ui.generating:
639
+ if _sel.select([fd], [], [], 0.3)[0]:
640
+ c = os.read(fd, 1).decode('latin-1')
641
+ running = handle_input(c, fd)
642
+ else:
643
+ if _sel.select([fd], [], [], 0.5)[0]:
644
+ c = os.read(fd, 1).decode('latin-1')
645
+ running = handle_input(c, fd)
646
+ render()
647
+ finally:
648
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
649
+ sys.stdout.write('\033[?25h\033[2J\033[H')
650
+ sys.stdout.flush()
651
+
652
+ # Print summary
653
+ if ui.discussion_log:
654
+ print(f'\033[1;36m=== CONVENE: {topic} ===\033[0m\n')
655
+ print(f'Participants: {", ".join(npc_names)}')
656
+ print(f'Rounds: {ui.round}/{ui.total_rounds}')
657
+ print(f'Contributions: {len(ui.discussion_log)}')
658
+ if ui.synthesis:
659
+ print(f'\n\033[1;32m--- Synthesis ---\033[0m')
660
+ print(ui.synthesis[:2000])
661
+
662
+ context['output'] = ui.synthesis or 'Convene session ended.'
663
+ context['messages'] = messages
664
+ context['convene_result'] = {
665
+ 'topic': topic,
666
+ 'participants': npc_names,
667
+ 'rounds': ui.round,
668
+ 'discussion': ui.discussion_log,
669
+ 'synthesis': ui.synthesis,
670
+ }