npcsh 1.1.21__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 (188) hide show
  1. npcsh/_state.py +282 -125
  2. npcsh/benchmark/npcsh_agent.py +77 -232
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +8 -6
  7. npcsh/npc_team/corca.npc +5 -12
  8. npcsh/npc_team/frederic.npc +6 -9
  9. npcsh/npc_team/guac.npc +4 -4
  10. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
  12. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  14. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  15. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  16. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  17. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  19. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  20. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  21. npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
  22. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  23. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  24. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  25. npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
  26. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  27. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  28. npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
  29. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  30. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  31. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  32. npcsh/npc_team/kadiefa.npc +6 -6
  33. npcsh/npc_team/npcsh.ctx +16 -0
  34. npcsh/npc_team/plonk.npc +5 -9
  35. npcsh/npc_team/sibiji.npc +15 -7
  36. npcsh/npcsh.py +1 -0
  37. npcsh/routes.py +0 -4
  38. npcsh/yap.py +22 -4
  39. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  40. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  41. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
  42. npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
  43. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  44. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  45. npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
  47. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  49. npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
  51. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  53. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  54. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  56. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
  57. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
  58. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
  60. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  61. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  62. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  64. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
  65. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  66. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  67. npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
  68. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
  69. npcsh-1.1.23.dist-info/RECORD +216 -0
  70. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  71. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  72. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  74. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  75. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  76. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  77. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  78. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  79. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  80. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  81. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  82. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  83. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  84. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  85. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  86. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  87. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  88. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  89. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  90. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  91. npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  92. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  93. npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  94. npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  95. npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
  96. npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
  97. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  98. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  99. npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  100. npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
  101. npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
  102. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  103. npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  104. npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
  105. npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
  106. npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  107. npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  108. npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  109. npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
  110. npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  111. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  112. npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
  113. npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
  114. npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  115. npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  116. npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  117. npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
  118. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  119. npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  120. npcsh-1.1.21.dist-info/RECORD +0 -243
  121. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  122. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  133. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  134. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  135. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  136. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  137. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  138. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  139. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  140. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  141. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  142. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  143. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  144. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  145. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  146. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  147. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  148. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  149. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  150. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  152. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  153. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  154. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  155. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  156. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  157. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  158. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  159. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  160. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  161. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  162. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  163. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  164. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  165. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  166. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  167. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  168. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  169. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  170. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  171. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  172. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  173. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  174. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  175. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  181. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  182. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  183. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  184. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  185. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  186. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  187. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  188. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -1,232 +0,0 @@
1
- jinx_name: convene
2
- description: Run a cycle of discussions between NPCs on a topic. The orchestrator convenes agents to discuss and synthesize.
3
- inputs:
4
- - topic: ""
5
- - npcs: "alicanto,corca,guac"
6
- - rounds: 3
7
- - model: null
8
- - provider: null
9
- steps:
10
- - name: convene_discussion
11
- engine: python
12
- code: |
13
- from termcolor import colored
14
- from npcpy.llm_funcs import get_llm_response
15
-
16
- topic = context.get('topic', '')
17
- npcs_str = context.get('npcs', 'alicanto,corca,guac')
18
- rounds = int(context.get('rounds', 3))
19
-
20
- npc = context.get('npc')
21
- team = context.get('team')
22
- messages = context.get('messages', [])
23
-
24
- model = context.get('model') or (npc.model if npc else (state.chat_model if state else 'llama3.2'))
25
- provider = context.get('provider') or (npc.provider if npc else (state.chat_provider if state else 'ollama'))
26
-
27
- if not topic:
28
- context['output'] = """Usage: /convene <topic>
29
-
30
- Options:
31
- --npcs LIST Comma-separated NPC names (default: alicanto,corca,guac)
32
- --rounds N Number of discussion rounds (default: 3)
33
-
34
- Example: /convene "How should we approach the database migration?" --npcs corca,guac,frederic
35
- """
36
- exit()
37
-
38
- npc_names = [n.strip() for n in npcs_str.split(',')]
39
-
40
- print(f"""
41
- ██████ ██████ ███ ██ ██ ██ ███████ ███ ██ ███████
42
- ██ ██ ██ ████ ██ ██ ██ ██ ████ ██ ██
43
- ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ █████
44
- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
45
- ██████ ██████ ██ ████ ████ ███████ ██ ████ ███████
46
-
47
- Convening Discussion
48
- Topic: {topic}
49
- Participants: {', '.join(npc_names)}
50
- Rounds: {rounds}
51
- """)
52
-
53
- # Get NPC personas
54
- participants = []
55
- for name in npc_names:
56
- if team and hasattr(team, 'npcs') and name in team.npcs:
57
- target_npc = team.npcs[name]
58
- persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
59
- participants.append({'name': name, 'persona': persona, 'npc': target_npc})
60
- else:
61
- participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
62
-
63
- import random
64
-
65
- discussion_log = []
66
-
67
- for round_num in range(1, rounds + 1):
68
- print(colored(f"\n{'='*60}", "cyan"))
69
- print(colored(f" ROUND {round_num}/{rounds}", "cyan", attrs=["bold"]))
70
- print(colored(f"{'='*60}", "cyan"))
71
-
72
- round_contributions = []
73
-
74
- for participant in participants:
75
- name = participant['name']
76
- persona = participant['persona']
77
-
78
- # Build context from previous contributions
79
- prev_context = ""
80
- if discussion_log:
81
- prev_context = "\n\nPrevious discussion:\n"
82
- for entry in discussion_log[-len(participants)*2:]:
83
- prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
84
-
85
- if round_contributions:
86
- prev_context += "\nThis round so far:\n"
87
- for entry in round_contributions:
88
- prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
89
-
90
- prompt = f"""You are {name}. {persona}
91
-
92
- Topic under discussion: "{topic}"
93
- {prev_context}
94
-
95
- Provide your perspective on this topic. Be concise but insightful.
96
- Build on what others have said if applicable.
97
- If you disagree with something, explain why constructively.
98
- """
99
-
100
- print(colored(f"\n[{name}]:", "yellow", attrs=["bold"]))
101
-
102
- resp = get_llm_response(
103
- prompt,
104
- model=model,
105
- provider=provider,
106
- npc=participant.get('npc') or npc,
107
- temperature=0.7
108
- )
109
-
110
- contribution = str(resp.get('response', ''))
111
- print(contribution)
112
-
113
- entry = {
114
- 'round': round_num,
115
- 'speaker': name,
116
- 'contribution': contribution
117
- }
118
- round_contributions.append(entry)
119
- discussion_log.append(entry)
120
-
121
- # Sample a followup from another participant
122
- other_participants = [p for p in participants if p['name'] != name]
123
- if other_participants:
124
- followup_participant = random.choice(other_participants)
125
- followup_name = followup_participant['name']
126
- followup_persona = followup_participant['persona']
127
-
128
- followup_prompt = f"""You are {followup_name}. {followup_persona}
129
-
130
- Topic: "{topic}"
131
-
132
- {name} just said: "{contribution[:500]}"
133
-
134
- Respond briefly to this specific point - agree, disagree, build on it, or ask a clarifying question.
135
- Keep it to 2-3 sentences.
136
- """
137
-
138
- print(colored(f"\n [{followup_name} responds]:", "cyan"))
139
-
140
- followup_resp = get_llm_response(
141
- followup_prompt,
142
- model=model,
143
- provider=provider,
144
- npc=followup_participant.get('npc') or npc,
145
- temperature=0.7
146
- )
147
-
148
- followup_contribution = str(followup_resp.get('response', ''))
149
- print(f" {followup_contribution}")
150
-
151
- discussion_log.append({
152
- 'round': round_num,
153
- 'speaker': followup_name,
154
- 'contribution': followup_contribution,
155
- 'type': 'followup'
156
- })
157
-
158
- # Probability of original speaker responding back vs someone else
159
- if random.random() < 0.4:
160
- # Original speaker responds
161
- responder = participant
162
- responder_name = name
163
- else:
164
- # Sample from others (could be followup person or someone else)
165
- responder = random.choice(other_participants)
166
- responder_name = responder['name']
167
-
168
- if random.random() < 0.6: # 60% chance of a counter-response
169
- counter_prompt = f"""You are {responder_name}. {responder['persona']}
170
-
171
- Topic: "{topic}"
172
-
173
- {followup_name} responded: "{followup_contribution}"
174
-
175
- Brief reaction (1-2 sentences). Move the discussion forward.
176
- """
177
-
178
- print(colored(f"\n [{responder_name}]:", "magenta"))
179
-
180
- counter_resp = get_llm_response(
181
- counter_prompt,
182
- model=model,
183
- provider=provider,
184
- npc=responder.get('npc') or npc,
185
- temperature=0.7
186
- )
187
-
188
- counter_contribution = str(counter_resp.get('response', ''))
189
- print(f" {counter_contribution}")
190
-
191
- discussion_log.append({
192
- 'round': round_num,
193
- 'speaker': responder_name,
194
- 'contribution': counter_contribution,
195
- 'type': 'counter'
196
- })
197
-
198
- # Synthesis
199
- print(colored(f"\n{'='*60}", "green"))
200
- print(colored(" SYNTHESIS", "green", attrs=["bold"]))
201
- print(colored(f"{'='*60}", "green"))
202
-
203
- all_contributions = "\n".join([
204
- f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}"
205
- for e in discussion_log
206
- ])
207
-
208
- synthesis_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
209
-
210
- Full discussion:
211
- {all_contributions}
212
-
213
- Provide:
214
- 1. Key agreements and consensus points
215
- 2. Areas of disagreement or tension
216
- 3. Novel ideas that emerged
217
- 4. Recommended next steps or actions
218
- """
219
-
220
- resp = get_llm_response(synthesis_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
221
- synthesis = str(resp.get('response', ''))
222
- print(synthesis)
223
-
224
- context['output'] = synthesis
225
- context['messages'] = messages
226
- context['convene_result'] = {
227
- 'topic': topic,
228
- 'participants': npc_names,
229
- 'rounds': rounds,
230
- 'discussion': discussion_log,
231
- 'synthesis': synthesis
232
- }
@@ -1,429 +0,0 @@
1
- jinx_name: kg_search
2
- description: Search the knowledge graph with interactive TUI
3
- interactive: true
4
- inputs:
5
- - query: ""
6
- - type: "facts"
7
- - mode: "keyword"
8
- - concept: ""
9
- - depth: "2"
10
- - breadth: "5"
11
- - max_results: "20"
12
- - threshold: "0.6"
13
- - npc_name: ""
14
- - team_name: ""
15
- - db_path: ""
16
- - text: "false"
17
-
18
- steps:
19
- - name: search_kg
20
- engine: python
21
- code: |
22
- import os
23
- import sys
24
- import tty
25
- import termios
26
- from npcpy.memory.command_history import CommandHistory
27
- from npcpy.memory.knowledge_graph import (
28
- kg_search_facts, kg_list_concepts, kg_get_facts_for_concept,
29
- kg_get_all_facts, kg_link_search, kg_embedding_search,
30
- kg_hybrid_search, kg_explore_concept
31
- )
32
-
33
- query = context.get('query', '').strip()
34
- search_type = context.get('type', 'facts').lower()
35
- search_mode = context.get('mode', 'keyword').lower()
36
- concept = context.get('concept', '').strip()
37
- max_depth = int(context.get('depth') or 2)
38
- breadth = int(context.get('breadth') or 5)
39
- max_results = int(context.get('max_results') or 20)
40
- threshold = float(context.get('threshold') or 0.6)
41
- text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
42
-
43
- if not query and search_type == 'facts' and not concept:
44
- lines = [
45
- "Usage: /kg_search <query> [mode=keyword|embedding|link|hybrid|all]",
46
- "",
47
- "Search Modes:",
48
- " keyword - Simple keyword matching (default, fast)",
49
- " embedding - Semantic similarity using embeddings",
50
- " link - Traverse graph links from keyword matches",
51
- " hybrid - Combine keyword + link traversal",
52
- " all - Combine all methods (slowest, most thorough)",
53
- "",
54
- "Options:",
55
- " type - Result type: facts, concepts, all",
56
- " concept - Explore a specific concept",
57
- " depth - Link traversal depth (default: 2)",
58
- " breadth - Results per traversal hop (default: 5)",
59
- " max_results - Max total results (default: 20)",
60
- " threshold - Embedding similarity threshold (default: 0.6)",
61
- " text - Text-only output, no TUI (true/false)",
62
- "",
63
- "TUI Controls:",
64
- " j/k or arrows - Navigate",
65
- " 1/2/3 - Sort by score/concept/type",
66
- " c - Toggle concepts view",
67
- " e - Explore selected concept",
68
- " p - Preview full fact/concept",
69
- " q/ESC - Quit",
70
- "",
71
- "Examples:",
72
- " /kg_search python",
73
- " /kg_search python mode=embedding",
74
- " /kg_search python mode=link depth=3",
75
- " /kg_search type=concepts",
76
- " /kg_search concept=coding",
77
- ]
78
- context['output'] = "\n".join(lines)
79
- else:
80
- db_path = context.get('db_path') or os.path.expanduser("~/npcsh_history.db")
81
-
82
- try:
83
- cmd_history = CommandHistory(db_path)
84
- engine = cmd_history.engine
85
-
86
- team_obj = None
87
- try:
88
- team_obj = state.team if 'state' in dir() and state else None
89
- except:
90
- pass
91
- npc_obj = npc if 'npc' in dir() else None
92
-
93
- emodel = None
94
- eprovider = None
95
- try:
96
- if 'state' in dir() and state:
97
- emodel = getattr(state, 'embedding_model', None)
98
- eprovider = getattr(state, 'embedding_provider', None)
99
- except:
100
- pass
101
-
102
- # Collect results
103
- results = []
104
-
105
- if concept:
106
- result = kg_explore_concept(engine, concept, max_depth=max_depth, breadth_per_step=breadth)
107
- for i, f in enumerate(result.get('direct_facts', [])):
108
- results.append({'type': 'fact', 'content': str(f), 'score': 1.0, 'concept': concept, 'source': 'direct'})
109
- for i, f in enumerate(result.get('extended_facts', [])):
110
- results.append({'type': 'fact', 'content': str(f), 'score': 0.5, 'concept': concept, 'source': 'extended'})
111
- for c in result.get('related_concepts', []):
112
- results.append({'type': 'concept', 'content': str(c), 'score': 0.8, 'concept': concept, 'source': 'related'})
113
-
114
- elif search_type == 'concepts':
115
- concepts = kg_list_concepts(engine, search_all_scopes=True)
116
- for c in concepts:
117
- results.append({'type': 'concept', 'content': str(c), 'score': 1.0, 'concept': str(c), 'source': 'list'})
118
-
119
- elif search_type == 'all' and not query:
120
- facts = kg_get_all_facts(engine, search_all_scopes=True)
121
- for f in facts[:max_results]:
122
- results.append({'type': 'fact', 'content': str(f), 'score': 1.0, 'concept': '', 'source': 'all'})
123
-
124
- elif search_mode == 'embedding':
125
- raw_results = kg_embedding_search(
126
- engine, query,
127
- embedding_model=emodel, embedding_provider=eprovider,
128
- similarity_threshold=threshold, max_results=max_results,
129
- search_all_scopes=True
130
- )
131
- for r in raw_results:
132
- results.append({
133
- 'type': r.get('type', 'fact'),
134
- 'content': str(r.get('content', '')),
135
- 'score': r.get('score', 0),
136
- 'concept': r.get('concept', ''),
137
- 'source': 'embedding'
138
- })
139
-
140
- elif search_mode == 'link':
141
- raw_results = kg_link_search(
142
- engine, query,
143
- max_depth=max_depth, breadth_per_step=breadth, max_results=max_results,
144
- search_all_scopes=True
145
- )
146
- for r in raw_results:
147
- results.append({
148
- 'type': r.get('type', 'fact'),
149
- 'content': str(r.get('content', '')),
150
- 'score': r.get('score', 0),
151
- 'concept': r.get('concept', ''),
152
- 'source': f"link-d{r.get('depth', 0)}"
153
- })
154
-
155
- elif search_mode in ['hybrid', 'all', 'keyword+link', 'keyword+embedding']:
156
- raw_results = kg_hybrid_search(
157
- engine, query,
158
- mode=search_mode if search_mode != 'hybrid' else 'keyword+link',
159
- max_depth=max_depth, breadth_per_step=breadth, max_results=max_results,
160
- embedding_model=emodel, embedding_provider=eprovider,
161
- similarity_threshold=threshold,
162
- search_all_scopes=True
163
- )
164
- for r in raw_results:
165
- results.append({
166
- 'type': r.get('type', 'fact'),
167
- 'content': str(r.get('content', '')),
168
- 'score': r.get('score', 0),
169
- 'concept': r.get('concept', ''),
170
- 'source': r.get('source', 'hybrid')
171
- })
172
-
173
- else:
174
- # Default keyword search
175
- facts = kg_search_facts(engine, query, search_all_scopes=True)
176
- for f in facts[:max_results]:
177
- results.append({'type': 'fact', 'content': str(f), 'score': 1.0, 'concept': '', 'source': 'keyword'})
178
-
179
- if not results:
180
- context['output'] = f"No KG results found for '{query}'"
181
- elif text_mode:
182
- # Text-only output
183
- lines = [f"Found {len(results)} KG results:", ""]
184
- for i, r in enumerate(results, 1):
185
- score = f"{r['score']:.2f}" if isinstance(r['score'], float) else str(r['score'])
186
- lines.append(f"{i}. [{r['type']} {score}] {r['content'][:80]}")
187
- context['output'] = "\n".join(lines)
188
- else:
189
- # Interactive TUI mode
190
- def get_terminal_size():
191
- try:
192
- size = os.get_terminal_size()
193
- return size.columns, size.lines
194
- except:
195
- return 80, 24
196
-
197
- width, height = get_terminal_size()
198
- selected = 0
199
- scroll = 0
200
- list_height = height - 5
201
- mode = 'list'
202
- preview_scroll = 0
203
- sort_mode = 'score' # score, concept, type
204
- type_filter = 'all' # all, fact, concept
205
-
206
- def sort_results(results, sort_mode):
207
- if sort_mode == 'score':
208
- return sorted(results, key=lambda x: x.get('score', 0), reverse=True)
209
- elif sort_mode == 'concept':
210
- return sorted(results, key=lambda x: (x.get('concept', ''), -x.get('score', 0)))
211
- elif sort_mode == 'type':
212
- return sorted(results, key=lambda x: (x.get('type', ''), -x.get('score', 0)))
213
- return results
214
-
215
- def filter_results(results, type_filter):
216
- if type_filter == 'all':
217
- return results
218
- return [r for r in results if r.get('type') == type_filter]
219
-
220
- display_results = filter_results(sort_results(results, sort_mode), type_filter)
221
-
222
- fd = sys.stdin.fileno()
223
- old_settings = termios.tcgetattr(fd)
224
-
225
- try:
226
- tty.setcbreak(fd)
227
- sys.stdout.write('\033[?25l')
228
- sys.stdout.write('\033[2J\033[H')
229
-
230
- while True:
231
- width, height = get_terminal_size()
232
- list_height = height - 5
233
-
234
- if mode == 'list':
235
- if selected < scroll:
236
- scroll = selected
237
- elif selected >= scroll + list_height:
238
- scroll = selected - list_height + 1
239
-
240
- sys.stdout.write('\033[H')
241
-
242
- # Header
243
- if mode == 'list':
244
- sort_ind = {'score': '1', 'concept': '2', 'type': '3'}[sort_mode]
245
- label = query or concept or search_type
246
- header = f" KG SEARCH ({len(display_results)} results): '{label}' [sort:{sort_mode}({sort_ind}) filter:{type_filter}] "
247
- else:
248
- header = f" PREVIEW: {display_results[selected]['type']} "
249
- sys.stdout.write(f'\033[7;1m{header.ljust(width)}\033[0m\n')
250
-
251
- # Column headers
252
- if mode == 'list':
253
- col_header = f' {"TYPE":<8} {"SCORE":<6} {"CONCEPT":<15} {"CONTENT":<50}'
254
- sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
255
- else:
256
- sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
257
-
258
- if mode == 'list':
259
- for i in range(list_height):
260
- idx = scroll + i
261
- sys.stdout.write(f'\033[{3+i};1H\033[K')
262
- if idx >= len(display_results):
263
- continue
264
-
265
- r = display_results[idx]
266
- rtype = r.get('type', '?')[:8]
267
- score = f"{r.get('score', 0):.2f}" if isinstance(r.get('score'), float) else str(r.get('score', ''))[:6]
268
- concept_str = (r.get('concept', '') or '')[:15]
269
- content = (r.get('content', '') or '')[:60].replace('\n', ' ')
270
-
271
- # Color by type
272
- if r.get('type') == 'concept':
273
- type_color = '\033[35m' # magenta
274
- else:
275
- type_color = '\033[36m' # cyan
276
-
277
- line = f" {type_color}{rtype:<8}\033[0m {score:<6} {concept_str:<15} {content}"
278
- line = line[:width+15]
279
-
280
- if idx == selected:
281
- sys.stdout.write(f'\033[7;1m>{line}\033[0m')
282
- else:
283
- sys.stdout.write(f' {line}')
284
-
285
- # Status bar
286
- sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
287
- sel = display_results[selected] if display_results else {}
288
- source = sel.get('source', '')
289
- sys.stdout.write(f'\033[{height-1};1H\033[K Source: {source}'.ljust(width))
290
- sys.stdout.write(f'\033[{height};1H\033[K\033[7m j/k:Nav 1/2/3:Sort c:Concepts e:Explore p:Preview q:Quit [{selected+1}/{len(display_results)}] \033[0m')
291
-
292
- else: # preview mode
293
- sel = display_results[selected]
294
- content = sel.get('content', '')
295
- lines = content.split('\n')
296
-
297
- # Add metadata at top
298
- meta_lines = [
299
- f"Type: {sel.get('type', '')}",
300
- f"Score: {sel.get('score', '')}",
301
- f"Concept: {sel.get('concept', '')}",
302
- f"Source: {sel.get('source', '')}",
303
- "─" * 40,
304
- ]
305
- all_lines = meta_lines + lines
306
-
307
- for i in range(list_height):
308
- idx = preview_scroll + i
309
- sys.stdout.write(f'\033[{3+i};1H\033[K')
310
- if idx < len(all_lines):
311
- sys.stdout.write(all_lines[idx][:width-1])
312
-
313
- sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
314
- sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(all_lines)} lines]')
315
- sys.stdout.write(f'\033[{height};1H\033[K\033[7m j/k:Scroll b:Back e:Explore q:Quit \033[0m')
316
-
317
- sys.stdout.flush()
318
-
319
- c = os.read(fd, 1).decode('latin-1')
320
-
321
- if c == '\x1b':
322
- import select as _sel
323
- if _sel.select([fd], [], [], 0.05)[0]:
324
- c2 = os.read(fd, 1).decode('latin-1')
325
- else:
326
- if mode == 'preview':
327
- mode = 'list'
328
- sys.stdout.write('\033[2J\033[H')
329
- else:
330
- context['output'] = "Cancelled."
331
- break
332
- continue
333
- if c2 == '[':
334
- c3 = os.read(fd, 1).decode('latin-1')
335
- if c3 == 'A': # Up
336
- if mode == 'list' and selected > 0:
337
- selected -= 1
338
- elif mode == 'preview' and preview_scroll > 0:
339
- preview_scroll -= 1
340
- elif c3 == 'B': # Down
341
- if mode == 'list' and selected < len(display_results) - 1:
342
- selected += 1
343
- elif mode == 'preview':
344
- sel = display_results[selected]
345
- content = sel.get('content', '')
346
- all_lines = content.split('\n')
347
- if preview_scroll < max(0, len(all_lines) + 5 - list_height):
348
- preview_scroll += 1
349
- else:
350
- if mode == 'preview':
351
- mode = 'list'
352
- sys.stdout.write('\033[2J\033[H')
353
- else:
354
- context['output'] = "Cancelled."
355
- break
356
- continue
357
-
358
- if c == 'q' or c == '\x03':
359
- context['output'] = "Cancelled."
360
- break
361
- elif c == 'k':
362
- if mode == 'list' and selected > 0:
363
- selected -= 1
364
- elif mode == 'preview' and preview_scroll > 0:
365
- preview_scroll -= 1
366
- elif c == 'j':
367
- if mode == 'list' and selected < len(display_results) - 1:
368
- selected += 1
369
- elif mode == 'preview':
370
- sel = display_results[selected]
371
- content = sel.get('content', '')
372
- all_lines = content.split('\n')
373
- if preview_scroll < max(0, len(all_lines) + 5 - list_height):
374
- preview_scroll += 1
375
- elif c == '1':
376
- sort_mode = 'score'
377
- display_results = filter_results(sort_results(results, sort_mode), type_filter)
378
- selected = 0
379
- scroll = 0
380
- elif c == '2':
381
- sort_mode = 'concept'
382
- display_results = filter_results(sort_results(results, sort_mode), type_filter)
383
- selected = 0
384
- scroll = 0
385
- elif c == '3':
386
- sort_mode = 'type'
387
- display_results = filter_results(sort_results(results, sort_mode), type_filter)
388
- selected = 0
389
- scroll = 0
390
- elif c == 'c' and mode == 'list':
391
- # Toggle type filter
392
- if type_filter == 'all':
393
- type_filter = 'concept'
394
- elif type_filter == 'concept':
395
- type_filter = 'fact'
396
- else:
397
- type_filter = 'all'
398
- display_results = filter_results(sort_results(results, sort_mode), type_filter)
399
- selected = 0
400
- scroll = 0
401
- elif c == 'e' and display_results:
402
- # Explore the selected concept
403
- sel = display_results[selected]
404
- explore_concept = sel.get('concept') or sel.get('content', '')
405
- if sel.get('type') == 'concept':
406
- explore_concept = sel.get('content', '')
407
- context['output'] = f"Explore concept: {explore_concept}\n\nRun: /kg_search concept={explore_concept}"
408
- break
409
- elif c == 'p' and mode == 'list' and display_results:
410
- mode = 'preview'
411
- preview_scroll = 0
412
- sys.stdout.write('\033[2J\033[H')
413
- elif c == 'b' and mode == 'preview':
414
- mode = 'list'
415
- sys.stdout.write('\033[2J\033[H')
416
- elif c in ('\r', '\n') and display_results:
417
- sel = display_results[selected]
418
- context['output'] = f"Selected: {sel.get('content', '')[:100]}"
419
- break
420
-
421
- finally:
422
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
423
- sys.stdout.write('\033[?25h')
424
- sys.stdout.write('\033[2J\033[H')
425
- sys.stdout.flush()
426
-
427
- except Exception as e:
428
- import traceback
429
- context['output'] = "KG search error: " + str(e) + "\n" + traceback.format_exc()