npcsh 1.1.19__py3-none-any.whl → 1.1.21__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.
- npcsh/_state.py +16 -78
- npcsh/diff_viewer.py +3 -3
- npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
- npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +18 -7
- npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +18 -7
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +20 -9
- npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +53 -15
- npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +393 -317
- npcsh/npc_team/jinxs/lib/utils/models.jinx +343 -0
- npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +8 -7
- npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
- npcsh/npc_team/jinxs/modes/arxiv.jinx +6 -6
- npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +4 -4
- npcsh/npc_team/jinxs/modes/git.jinx +795 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
- npcsh/npc_team/jinxs/modes/kg.jinx +941 -0
- npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
- npcsh/npc_team/jinxs/modes/nql.jinx +460 -0
- npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
- npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
- npcsh/npc_team/jinxs/modes/reattach.jinx +4 -4
- npcsh/npc_team/jinxs/modes/spool.jinx +4 -4
- npcsh/npc_team/jinxs/modes/team.jinx +504 -0
- npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +455 -182
- npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
- npcsh/npcsh.py +112 -47
- npcsh/routes.py +12 -3
- npcsh/salmon_simulation.py +0 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +6 -6
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
- npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +4 -4
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +18 -7
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +18 -7
- npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +4 -4
- npcsh-1.1.21.data/data/npcsh/npc_team/jinxs.jinx +407 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/kg.jinx +941 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +20 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/models.jinx +343 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/nql.jinx +460 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +1 -1
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +4 -4
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +8 -7
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +4 -4
- npcsh-1.1.21.data/data/npcsh/npc_team/team.jinx +504 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +53 -15
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
- {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
- {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/RECORD +147 -148
- npcsh-1.1.21.dist-info/entry_points.txt +11 -0
- npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -299
- npcsh/npc_team/jinxs/bin/memories.jinx +0 -316
- npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
- npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
- npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
- npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
- npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
- npcsh/npc_team/plonkjr.npc +0 -23
- npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +0 -356
- npcsh-1.1.19.data/data/npcsh/npc_team/compress.jinx +0 -140
- npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +0 -299
- npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
- npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +0 -73
- npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +0 -388
- npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +0 -316
- npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
- npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +0 -412
- npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +0 -379
- npcsh-1.1.19.data/data/npcsh/npc_team/plonkjr.npc +0 -23
- npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
- npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
- npcsh-1.1.19.data/data/npcsh/npc_team/vixynt.jinx +0 -122
- npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +0 -455
- npcsh-1.1.19.dist-info/entry_points.txt +0 -22
- /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
- /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
- /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
- /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
- /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
- {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
- {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
jinx_name: alicanto
|
|
2
|
-
description: Deep research mode - multi-
|
|
2
|
+
description: Deep research mode - multi-agent hypothesis exploration with approval-gated TUI pipeline
|
|
3
|
+
interactive: true
|
|
3
4
|
npc: forenpc
|
|
4
5
|
inputs:
|
|
5
6
|
- query: null
|
|
6
|
-
- num_npcs:
|
|
7
|
-
- depth: 3
|
|
7
|
+
- num_npcs: 3
|
|
8
8
|
- model: null
|
|
9
9
|
- provider: null
|
|
10
|
-
- max_steps:
|
|
11
|
-
-
|
|
12
|
-
- exploration: 0.3
|
|
13
|
-
- creativity: 0.5
|
|
10
|
+
- max_steps: 10
|
|
11
|
+
- num_cycles: 3
|
|
14
12
|
- format: report
|
|
15
|
-
- browse: false
|
|
16
13
|
|
|
17
14
|
steps:
|
|
18
15
|
- name: alicanto_research
|
|
@@ -22,335 +19,1615 @@ steps:
|
|
|
22
19
|
import sys
|
|
23
20
|
import tty
|
|
24
21
|
import termios
|
|
25
|
-
|
|
22
|
+
import select as _sel
|
|
23
|
+
import json
|
|
24
|
+
import random
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
import textwrap
|
|
28
|
+
import csv
|
|
29
|
+
import subprocess
|
|
30
|
+
import hashlib
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
from dataclasses import dataclass, asdict, field
|
|
33
|
+
from typing import List, Dict, Any, Tuple
|
|
34
|
+
from pathlib import Path
|
|
26
35
|
|
|
27
36
|
from npcpy.llm_funcs import get_llm_response
|
|
28
|
-
from npcpy.data.web import search_web
|
|
29
37
|
from npcpy.npc_compiler import NPC
|
|
30
38
|
|
|
39
|
+
try:
|
|
40
|
+
from npcpy.data.web import search_web
|
|
41
|
+
WEB_AVAILABLE = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
WEB_AVAILABLE = False
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from npcsh.wander import perform_single_wandering
|
|
47
|
+
WANDER_AVAILABLE = True
|
|
48
|
+
except ImportError:
|
|
49
|
+
WANDER_AVAILABLE = False
|
|
50
|
+
|
|
31
51
|
npc = context.get('npc')
|
|
32
52
|
team = context.get('team')
|
|
33
53
|
messages = context.get('messages', [])
|
|
34
54
|
|
|
35
|
-
# Resolve npc if it's a string (npc name) rather than NPC object
|
|
36
55
|
if isinstance(npc, str) and team:
|
|
37
56
|
npc = team.get(npc) if hasattr(team, 'get') else None
|
|
38
57
|
elif isinstance(npc, str):
|
|
39
|
-
npc = None
|
|
58
|
+
npc = None
|
|
59
|
+
|
|
60
|
+
query = context.get('query')
|
|
61
|
+
num_npcs = int(context.get('num_npcs', 3))
|
|
62
|
+
max_steps = int(context.get('max_steps', 10))
|
|
63
|
+
num_cycles = int(context.get('num_cycles', 3))
|
|
64
|
+
output_format = context.get('format', 'report')
|
|
65
|
+
|
|
66
|
+
model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
|
|
67
|
+
provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
|
|
40
68
|
|
|
41
|
-
# ==========
|
|
42
|
-
def
|
|
69
|
+
# ========== Utility ==========
|
|
70
|
+
def get_size():
|
|
43
71
|
try:
|
|
44
|
-
|
|
45
|
-
return
|
|
72
|
+
s = os.get_terminal_size()
|
|
73
|
+
return s.columns, s.lines
|
|
46
74
|
except:
|
|
47
75
|
return 80, 24
|
|
48
76
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
def wrap_text(text, width):
|
|
78
|
+
lines = []
|
|
79
|
+
for line in str(text).split('\n'):
|
|
80
|
+
if len(line) <= width:
|
|
81
|
+
lines.append(line)
|
|
82
|
+
else:
|
|
83
|
+
lines.extend(textwrap.wrap(line, width) or [''])
|
|
84
|
+
return lines
|
|
56
85
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
current_tab = 0
|
|
86
|
+
def clamp(val, lo, hi):
|
|
87
|
+
return max(lo, min(val, hi))
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
# ========== Data Classes (matching original) ==========
|
|
90
|
+
@dataclass
|
|
91
|
+
class ResearchStep:
|
|
92
|
+
step: int
|
|
93
|
+
thought: str
|
|
94
|
+
action: str
|
|
95
|
+
outcome: str
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class SubAgentTrace:
|
|
99
|
+
hypothesis: str
|
|
100
|
+
agent_name: str
|
|
101
|
+
agent_persona: str
|
|
102
|
+
steps: List[ResearchStep] = field(default_factory=list)
|
|
103
|
+
final_files: Dict[str, str] = field(default_factory=dict)
|
|
104
|
+
was_successful: bool = False
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
106
|
+
@dataclass
|
|
107
|
+
class Paper:
|
|
108
|
+
title: str = ""
|
|
109
|
+
abstract: str = ""
|
|
110
|
+
introduction: List[str] = field(default_factory=list)
|
|
111
|
+
methods: List[str] = field(default_factory=list)
|
|
112
|
+
results: List[str] = field(default_factory=list)
|
|
113
|
+
discussion: List[str] = field(default_factory=list)
|
|
68
114
|
|
|
115
|
+
# ========== Tool Definitions (matching original) ==========
|
|
116
|
+
_work_dir = os.path.join(os.getcwd(), 'alicanto_output')
|
|
117
|
+
os.makedirs(_work_dir, exist_ok=True)
|
|
118
|
+
_orig_cwd = os.getcwd()
|
|
119
|
+
os.chdir(_work_dir)
|
|
120
|
+
|
|
121
|
+
def create_file(filename: str, content: str) -> str:
|
|
122
|
+
"""Create a new file with the given content."""
|
|
123
|
+
filepath = os.path.abspath(filename)
|
|
124
|
+
if os.path.exists(filepath):
|
|
125
|
+
return f"Error: File '{filename}' already exists. Use append_to_file or replace_in_file to modify."
|
|
126
|
+
os.makedirs(os.path.dirname(filepath) if os.path.dirname(filepath) else '.', exist_ok=True)
|
|
127
|
+
with open(filepath, 'w') as f:
|
|
128
|
+
f.write(content)
|
|
129
|
+
return f"File '{filename}' created successfully."
|
|
130
|
+
|
|
131
|
+
def append_to_file(filename: str, content: str) -> str:
|
|
132
|
+
"""Append content to an existing file."""
|
|
133
|
+
filepath = os.path.abspath(filename)
|
|
134
|
+
if not os.path.exists(filepath):
|
|
135
|
+
return f"Error: File '{filename}' not found. Use create_file first."
|
|
136
|
+
with open(filepath, 'a') as f:
|
|
137
|
+
f.write("\n" + content)
|
|
138
|
+
return f"Content appended to '{filename}'."
|
|
139
|
+
|
|
140
|
+
def replace_in_file(filename: str, old_content: str, new_content: str) -> str:
|
|
141
|
+
"""Replace old_content with new_content in a file."""
|
|
142
|
+
filepath = os.path.abspath(filename)
|
|
143
|
+
if not os.path.exists(filepath):
|
|
144
|
+
return f"Error: File '{filename}' not found."
|
|
145
|
+
with open(filepath, 'r') as f:
|
|
146
|
+
file_contents = f.read()
|
|
147
|
+
file_contents = file_contents.replace(old_content, new_content)
|
|
148
|
+
with open(filepath, 'w') as f:
|
|
149
|
+
f.write(file_contents)
|
|
150
|
+
return f"Content in '{filename}' replaced."
|
|
151
|
+
|
|
152
|
+
def read_file(filename: str) -> str:
|
|
153
|
+
"""Read and return the contents of a file."""
|
|
154
|
+
filepath = os.path.abspath(filename)
|
|
155
|
+
if not os.path.exists(filepath):
|
|
156
|
+
return f"Error: File '{filename}' not found."
|
|
157
|
+
with open(filepath, 'r') as f:
|
|
158
|
+
return f.read()
|
|
159
|
+
|
|
160
|
+
def list_files(directory: str = ".") -> str:
|
|
161
|
+
"""List files in the current directory."""
|
|
69
162
|
try:
|
|
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
|
-
tab_bar += f'\033[43;30;1m {tab} \033[0m '
|
|
103
|
-
else:
|
|
104
|
-
tab_bar += f'\033[90m {tab} \033[0m '
|
|
105
|
-
sys.stdout.write(f'{tab_bar.ljust(width)}\n')
|
|
106
|
-
sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
|
|
107
|
-
|
|
108
|
-
# Content
|
|
109
|
-
for i in range(list_height):
|
|
110
|
-
idx = scroll + i
|
|
111
|
-
sys.stdout.write(f'\033[{3+i};1H\033[K')
|
|
112
|
-
if idx >= len(items):
|
|
113
|
-
continue
|
|
114
|
-
|
|
115
|
-
line = str(items[idx])[:width-2]
|
|
116
|
-
if current_tab in [0, 1, 2] and idx == selected:
|
|
117
|
-
sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
|
|
118
|
-
else:
|
|
119
|
-
# Color gold/cliff markers
|
|
120
|
-
if '[GOLD]' in line:
|
|
121
|
-
sys.stdout.write(f'\033[33m {line}\033[0m')
|
|
122
|
-
elif '[CLIFF]' in line:
|
|
123
|
-
sys.stdout.write(f'\033[31m {line}\033[0m')
|
|
124
|
-
else:
|
|
125
|
-
sys.stdout.write(f' {line}')
|
|
126
|
-
|
|
127
|
-
# Status bar
|
|
128
|
-
sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
|
|
129
|
-
counts = f"Gold: {len(gold)} | Cliffs: {len(cliffs)} | Insights: {len(insights)}"
|
|
130
|
-
sys.stdout.write(f'\033[{height-1};1H\033[K {counts}'.ljust(width)[:width])
|
|
131
|
-
sys.stdout.write(f'\033[{height};1H\033[K\033[43;30m h/l:Tabs j/k:Nav Enter:View q:Quit [{selected+1}/{len(items)}] \033[0m')
|
|
132
|
-
|
|
133
|
-
sys.stdout.flush()
|
|
134
|
-
|
|
135
|
-
c = sys.stdin.read(1)
|
|
136
|
-
|
|
137
|
-
if c == '\x1b':
|
|
138
|
-
c2 = sys.stdin.read(1)
|
|
139
|
-
if c2 == '[':
|
|
140
|
-
c3 = sys.stdin.read(1)
|
|
141
|
-
if c3 == 'A' and selected > 0:
|
|
142
|
-
selected -= 1
|
|
143
|
-
elif c3 == 'B' and selected < len(items) - 1:
|
|
144
|
-
selected += 1
|
|
145
|
-
elif c3 == 'C': # Right
|
|
146
|
-
current_tab = (current_tab + 1) % len(tabs)
|
|
147
|
-
selected = 0
|
|
148
|
-
scroll = 0
|
|
149
|
-
elif c3 == 'D': # Left
|
|
150
|
-
current_tab = (current_tab - 1) % len(tabs)
|
|
151
|
-
selected = 0
|
|
152
|
-
scroll = 0
|
|
163
|
+
return "\n".join(os.listdir(directory))
|
|
164
|
+
except:
|
|
165
|
+
return "Error listing directory."
|
|
166
|
+
|
|
167
|
+
def execute_shell_command(command: str) -> str:
|
|
168
|
+
"""Execute a shell command and return stdout/stderr."""
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60)
|
|
171
|
+
out = ""
|
|
172
|
+
if result.stdout:
|
|
173
|
+
out += result.stdout
|
|
174
|
+
if result.stderr:
|
|
175
|
+
out += result.stderr
|
|
176
|
+
return out[:3000] if out.strip() else "(no output)"
|
|
177
|
+
except subprocess.TimeoutExpired:
|
|
178
|
+
return "Command timed out (60s limit)"
|
|
179
|
+
except Exception as e:
|
|
180
|
+
return f"Error: {e}"
|
|
181
|
+
|
|
182
|
+
def _web_search_tool(query: str) -> str:
|
|
183
|
+
"""Search the web for information."""
|
|
184
|
+
if not WEB_AVAILABLE:
|
|
185
|
+
return "Web search not available."
|
|
186
|
+
try:
|
|
187
|
+
results = search_web(query, num_results=5)
|
|
188
|
+
if not results:
|
|
189
|
+
return "No results found."
|
|
190
|
+
if isinstance(results, list):
|
|
191
|
+
out = []
|
|
192
|
+
for r in results[:5]:
|
|
193
|
+
if isinstance(r, dict):
|
|
194
|
+
out.append(f"- {r.get('title', 'N/A')}: {str(r.get('content', r.get('snippet', '')))[:300]}")
|
|
153
195
|
else:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
elif c == 'k' and selected > 0:
|
|
160
|
-
selected -= 1
|
|
161
|
-
elif c == 'j' and selected < len(items) - 1:
|
|
162
|
-
selected += 1
|
|
163
|
-
elif c == 'h':
|
|
164
|
-
current_tab = (current_tab - 1) % len(tabs)
|
|
165
|
-
selected = 0
|
|
166
|
-
scroll = 0
|
|
167
|
-
elif c == 'l':
|
|
168
|
-
current_tab = (current_tab + 1) % len(tabs)
|
|
169
|
-
selected = 0
|
|
170
|
-
scroll = 0
|
|
171
|
-
elif c in ('\r', '\n') and current_tab < 3:
|
|
172
|
-
# Show full item
|
|
173
|
-
item = items[selected] if selected < len(items) else ''
|
|
174
|
-
print(f'\033[2J\033[H{item}\n\nPress any key to continue...')
|
|
175
|
-
sys.stdout.flush()
|
|
176
|
-
sys.stdin.read(1)
|
|
177
|
-
sys.stdout.write('\033[2J\033[H')
|
|
178
|
-
|
|
179
|
-
finally:
|
|
180
|
-
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
181
|
-
sys.stdout.write('\033[?25h')
|
|
182
|
-
sys.stdout.write('\033[2J\033[H')
|
|
183
|
-
sys.stdout.flush()
|
|
196
|
+
out.append(f"- {str(r)[:300]}")
|
|
197
|
+
return "\n".join(out) if out else str(results)[:2000]
|
|
198
|
+
return str(results)[:2000]
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return f"Search error: {e}"
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
# ========== File Provenance (matching original) ==========
|
|
203
|
+
@dataclass
|
|
204
|
+
class FileProvenance:
|
|
205
|
+
filename: str
|
|
206
|
+
step_history: List[Tuple[int, str, str, str]] = field(default_factory=list)
|
|
207
|
+
|
|
208
|
+
def get_filesystem_state() -> Dict[str, str]:
|
|
209
|
+
files = {}
|
|
210
|
+
for f in os.listdir("."):
|
|
211
|
+
if os.path.isfile(f):
|
|
212
|
+
with open(f, 'rb') as fh:
|
|
213
|
+
content = fh.read()
|
|
214
|
+
files[f] = hashlib.md5(content).hexdigest()[:8]
|
|
215
|
+
return files
|
|
216
|
+
|
|
217
|
+
def summarize_step(thought, action, outcome, fs_before, fs_after,
|
|
218
|
+
file_provenance, step_num, _model, _provider, _npc):
|
|
219
|
+
"""Advisor compresses a major step and suggests next action."""
|
|
220
|
+
current_files = {}
|
|
221
|
+
for f in os.listdir("."):
|
|
222
|
+
if os.path.isfile(f):
|
|
223
|
+
with open(f, 'rb') as fh:
|
|
224
|
+
content = fh.read()
|
|
225
|
+
current_files[f] = {'size': len(content), 'checksum': hashlib.md5(content).hexdigest()[:8]}
|
|
226
|
+
|
|
227
|
+
for f in fs_after:
|
|
228
|
+
if f not in file_provenance:
|
|
229
|
+
file_provenance[f] = FileProvenance(filename=f)
|
|
230
|
+
if f not in fs_before:
|
|
231
|
+
change = f"Created with {current_files.get(f, {}).get('size', '?')} bytes"
|
|
232
|
+
file_provenance[f].step_history.append((step_num, "CREATED", fs_after[f], change))
|
|
233
|
+
elif fs_before.get(f) != fs_after[f]:
|
|
234
|
+
change = f"Modified to {current_files.get(f, {}).get('size', '?')} bytes"
|
|
235
|
+
file_provenance[f].step_history.append((step_num, "MODIFIED", fs_after[f], change))
|
|
236
|
+
|
|
237
|
+
provenance_summary = []
|
|
238
|
+
for filename, prov in file_provenance.items():
|
|
239
|
+
history = "; ".join([f"Step {s}: {a} ({c}) - {ch}" for s, a, c, ch in prov.step_history])
|
|
240
|
+
provenance_summary.append(f"{filename}: {history}")
|
|
241
|
+
|
|
242
|
+
prompt = f"""AGENT'S REASONING: {str(thought)[:1500]}
|
|
243
|
+
|
|
244
|
+
AGENT'S ACTION: {str(action)[:1000]}
|
|
245
|
+
AGENT'S CLAIMED OUTCOME: {str(outcome)[:1000]}
|
|
246
|
+
|
|
247
|
+
COMPLETE FILE PROVENANCE:
|
|
248
|
+
{chr(10).join(provenance_summary)}
|
|
249
|
+
|
|
250
|
+
CURRENT FILESYSTEM:
|
|
251
|
+
Files: {list(current_files.keys())}
|
|
252
|
+
Details: {current_files}
|
|
253
|
+
|
|
254
|
+
Explain plainly what happened and whether the actions produced any measurable effects.
|
|
255
|
+
If the agent thinks then it is likely time to direct it to carry out a specific action.
|
|
256
|
+
|
|
257
|
+
Return JSON with "summary" and "next_step" keys."""
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
response = get_llm_response(prompt, model=_model, provider=_provider, npc=_npc, format='json')
|
|
261
|
+
summary_data = response.get('response')
|
|
262
|
+
if isinstance(summary_data, str):
|
|
263
|
+
summary_data = json.loads(summary_data)
|
|
264
|
+
if not isinstance(summary_data, dict):
|
|
265
|
+
summary_data = {"summary": str(summary_data), "next_step": "Continue research."}
|
|
266
|
+
except:
|
|
267
|
+
summary_data = {"summary": "Step completed.", "next_step": "Continue research."}
|
|
268
|
+
return summary_data
|
|
269
|
+
|
|
270
|
+
# ========== Persona Generation (matching original) ==========
|
|
271
|
+
def save_persona_to_csv(persona_data):
|
|
272
|
+
csv_dir = os.path.expanduser("~/.npcsh/npc_team")
|
|
273
|
+
os.makedirs(csv_dir, exist_ok=True)
|
|
274
|
+
csv_path = os.path.join(csv_dir, "alicanto_personas.csv")
|
|
275
|
+
file_exists = os.path.exists(csv_path)
|
|
276
|
+
with open(csv_path, 'a', newline='') as csvfile:
|
|
277
|
+
fieldnames = ['name', 'birth_year', 'location', 'leader', 'interests',
|
|
278
|
+
'worldview', 'approach', 'persona_text', 'created_at']
|
|
279
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
280
|
+
if not file_exists:
|
|
281
|
+
writer.writeheader()
|
|
282
|
+
row = dict(persona_data)
|
|
283
|
+
if isinstance(row.get('interests'), list):
|
|
284
|
+
row['interests'] = json.dumps(row['interests'])
|
|
285
|
+
row['created_at'] = datetime.now().isoformat()
|
|
286
|
+
writer.writerow(row)
|
|
287
|
+
|
|
288
|
+
def generate_one_persona(birth_year, _model, _provider, _npc):
|
|
289
|
+
"""Generate a single persona for a given birth year (original algorithm)."""
|
|
290
|
+
teen_year = birth_year + 16
|
|
291
|
+
json_template = (
|
|
292
|
+
'{\n'
|
|
293
|
+
' "name": "culturally appropriate full name for someone born in ' + str(birth_year) + '",\n'
|
|
294
|
+
' "location": "specific city/region where they were born in ' + str(birth_year) + '",\n'
|
|
295
|
+
' "leader": "who ruled their region when they were 16 years old in ' + str(teen_year) + '",\n'
|
|
296
|
+
' "interests": ["3-5 specific interests/obsessions they had as a teenager in ' + str(teen_year) + '"],\n'
|
|
297
|
+
' "worldview": "one sentence describing their fundamental perspective shaped by growing up in that era",\n'
|
|
298
|
+
' "approach": "how their historical background influences their way of thinking"\n'
|
|
299
|
+
'}'
|
|
300
|
+
)
|
|
301
|
+
prompt = (
|
|
302
|
+
f"Generate a unique persona for someone born in {birth_year}. Return JSON:\n"
|
|
303
|
+
f"{json_template}\n\n"
|
|
304
|
+
f"Make this person feel real and historically grounded. Consider: technological context, "
|
|
305
|
+
f"cultural movements, economic conditions, wars, discoveries happening in {teen_year}."
|
|
306
|
+
)
|
|
307
|
+
response = get_llm_response(prompt, model=_model, provider=_provider, npc=_npc, format='json')
|
|
308
|
+
new_persona = response.get('response')
|
|
309
|
+
if isinstance(new_persona, str):
|
|
310
|
+
raw = new_persona
|
|
311
|
+
start = raw.find('{')
|
|
312
|
+
end = raw.rfind('}') + 1
|
|
313
|
+
if start >= 0 and end > start:
|
|
314
|
+
raw = raw[start:end]
|
|
315
|
+
new_persona = json.loads(raw)
|
|
316
|
+
|
|
317
|
+
interests = new_persona.get('interests', [])
|
|
318
|
+
if isinstance(interests, list):
|
|
319
|
+
interests_str = ', '.join(interests)
|
|
320
|
+
else:
|
|
321
|
+
interests_str = str(interests)
|
|
322
|
+
|
|
323
|
+
persona_text = (
|
|
324
|
+
f"You are {new_persona.get('name')}, born {birth_year} in {new_persona.get('location')}, "
|
|
325
|
+
f"came of age under {new_persona.get('leader')}. "
|
|
326
|
+
f"Your interests were: {interests_str}. "
|
|
327
|
+
f"{new_persona.get('worldview')} {new_persona.get('approach')}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
persona_data = {
|
|
331
|
+
'name': new_persona.get('name', f'Agent'),
|
|
332
|
+
'birth_year': birth_year,
|
|
333
|
+
'location': new_persona.get('location', 'Unknown'),
|
|
334
|
+
'leader': new_persona.get('leader', 'Unknown'),
|
|
335
|
+
'interests': new_persona.get('interests', []),
|
|
336
|
+
'worldview': new_persona.get('worldview', ''),
|
|
337
|
+
'approach': new_persona.get('approach', ''),
|
|
338
|
+
'persona_text': persona_text,
|
|
339
|
+
}
|
|
340
|
+
try:
|
|
341
|
+
save_persona_to_csv(persona_data)
|
|
342
|
+
except:
|
|
343
|
+
pass
|
|
344
|
+
return persona_data
|
|
345
|
+
|
|
346
|
+
# ========== Sub-Agent Trace (matching original) ==========
|
|
347
|
+
def run_sub_agent_trace(hypothesis, persona_data, user_query, _model, _provider, _max_steps, ui_state):
|
|
348
|
+
"""Run one sub-agent trace matching original algorithm."""
|
|
349
|
+
agent_name = persona_data.get('name', 'Agent')
|
|
350
|
+
agent_persona = persona_data.get('persona_text', '')
|
|
351
|
+
|
|
352
|
+
# wander wrapper for when agent is stuck
|
|
353
|
+
def wander_wrapper(problem_description: str) -> str:
|
|
354
|
+
"""Get creative ideas when stuck using the wander exploration mode."""
|
|
355
|
+
if not WANDER_AVAILABLE:
|
|
356
|
+
return "Wander not available. Try a different approach."
|
|
357
|
+
try:
|
|
358
|
+
_, _, raw_brainstorm, _, _ = perform_single_wandering(
|
|
359
|
+
problem=problem_description, npc=agent, model=_model, provider=_provider
|
|
360
|
+
)
|
|
361
|
+
return str(raw_brainstorm)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
return f"Wander failed: {e}"
|
|
364
|
+
|
|
365
|
+
tools = [create_file, append_to_file, replace_in_file, read_file,
|
|
366
|
+
list_files, execute_shell_command, _web_search_tool, wander_wrapper]
|
|
367
|
+
|
|
368
|
+
agent = NPC(
|
|
369
|
+
name=agent_name.replace(' ', '_').lower(),
|
|
370
|
+
model=_model,
|
|
371
|
+
provider=_provider,
|
|
372
|
+
primary_directive=agent_persona,
|
|
373
|
+
tools=tools
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
trace = SubAgentTrace(hypothesis=hypothesis, agent_name=agent_name, agent_persona=agent_persona)
|
|
377
|
+
summarized_history = []
|
|
378
|
+
file_provenance = {}
|
|
379
|
+
created_files = set()
|
|
380
|
+
summary = {}
|
|
381
|
+
major_step = 0
|
|
382
|
+
|
|
383
|
+
while major_step < _max_steps:
|
|
384
|
+
# Check for skip/quit
|
|
385
|
+
if ui_state.get('skip'):
|
|
386
|
+
ui_state['skip'] = False
|
|
387
|
+
ui_state['log'].append(f"\033[33m Skipped by user\033[0m")
|
|
388
|
+
break
|
|
389
|
+
while ui_state.get('paused'):
|
|
390
|
+
time.sleep(0.2)
|
|
391
|
+
if ui_state.get('skip'):
|
|
392
|
+
break
|
|
393
|
+
|
|
394
|
+
fs_before = get_filesystem_state()
|
|
395
|
+
|
|
396
|
+
provenance_summary = []
|
|
397
|
+
for filename, prov in file_provenance.items():
|
|
398
|
+
history = "; ".join([f"Step {s}: {a} ({c}) - {ch}" for s, a, c, ch in prov.step_history])
|
|
399
|
+
provenance_summary.append(f"{filename}: {history}")
|
|
400
|
+
|
|
401
|
+
history_str = "\n".join(summarized_history)
|
|
402
|
+
next_step_text = f"This is the next step suggested by your advisor. : BEGIN NEXT_STEP: {summary.get('next_step')} END NEXT STEP" if summary else ""
|
|
403
|
+
|
|
404
|
+
search_provider = os.environ.get('NPCSH_SEARCH_PROVIDER', 'duckduckgo')
|
|
405
|
+
initial_prompt = f"""Test the following hypothesis: '{hypothesis}' as related to the user query: '{user_query}'.
|
|
406
|
+
Only focus on your specific hypothesis, other agents are being tasked with other aspects of the problem.
|
|
407
|
+
|
|
408
|
+
Use bash commands to carry out research through the execute_shell_command.
|
|
409
|
+
Adjust files with `replace_in_file` and use `read_file` and `list_files` to verify file states and file creation.
|
|
410
|
+
Create files with create_file()
|
|
411
|
+
|
|
412
|
+
Test with execute_shell_command when needed
|
|
413
|
+
Get unstuck with wander_wrapper
|
|
414
|
+
|
|
415
|
+
When you have a definitive result, say RESEARCH_COMPLETE.
|
|
416
|
+
|
|
417
|
+
FILE PROVENANCE HISTORY:
|
|
418
|
+
{chr(10).join(provenance_summary)}
|
|
419
|
+
|
|
420
|
+
CURRENT FILES: {list(fs_before.keys())}
|
|
421
|
+
|
|
422
|
+
COMPLETE ACTION HISTORY:
|
|
423
|
+
BEGIN HISTORY
|
|
424
|
+
{history_str}
|
|
425
|
+
END HISTORY
|
|
426
|
+
|
|
427
|
+
What specific action will you take next to test your hypothesis?
|
|
428
|
+
AVAILABLE TOOLS: create_file, append_to_file, replace_in_file, read_file, list_files, execute_shell_command, wander_wrapper, _web_search_tool.
|
|
429
|
+
|
|
430
|
+
Do not repeat actions. Use `_web_search_tool` with provider of {search_provider} to look up items if you are struggling.
|
|
431
|
+
|
|
432
|
+
{next_step_text}
|
|
433
|
+
|
|
434
|
+
Your goal is to research. To set up experiments, create figures, and produce data outputs in csvs for verification and reproducibility."""
|
|
435
|
+
|
|
436
|
+
ui_state['log'].append(f"\033[90m Major step {major_step + 1}\033[0m")
|
|
437
|
+
|
|
438
|
+
agent_messages = []
|
|
439
|
+
all_thoughts = []
|
|
440
|
+
all_actions = []
|
|
441
|
+
all_outcomes = []
|
|
442
|
+
|
|
443
|
+
for micro_step in range(5):
|
|
444
|
+
if ui_state.get('skip'):
|
|
445
|
+
break
|
|
446
|
+
|
|
447
|
+
if micro_step == 0:
|
|
448
|
+
current_prompt = initial_prompt
|
|
449
|
+
else:
|
|
450
|
+
current_prompt = "Continue your work. What's your next action?"
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
response = agent.get_llm_response(
|
|
454
|
+
current_prompt,
|
|
455
|
+
messages=agent_messages,
|
|
456
|
+
auto_process_tool_calls=True
|
|
457
|
+
)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
ui_state['log'].append(f" \033[31mLLM error: {str(e)[:80]}\033[0m")
|
|
460
|
+
break
|
|
461
|
+
|
|
462
|
+
agent_messages = response.get('messages', [])
|
|
463
|
+
thought = response.get('response')
|
|
464
|
+
if thought is None:
|
|
465
|
+
thought = ''
|
|
466
|
+
else:
|
|
467
|
+
all_thoughts.append(thought)
|
|
468
|
+
preview = thought.replace('\n', ' ')[:120]
|
|
469
|
+
ui_state['log'].append(f" {preview}")
|
|
470
|
+
|
|
471
|
+
if thought and "RESEARCH_COMPLETE" in thought.upper():
|
|
472
|
+
ui_state['log'].append(f" \033[32mRESEARCH_COMPLETE\033[0m")
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
if response.get('tool_results'):
|
|
476
|
+
tool_results = response['tool_results']
|
|
477
|
+
tool_names = []
|
|
478
|
+
outcomes = []
|
|
479
|
+
for res in tool_results:
|
|
480
|
+
tname = res.get('tool_name', '?')
|
|
481
|
+
tool_names.append(tname)
|
|
482
|
+
args = res.get('arguments', {})
|
|
483
|
+
if tname in ('create_file', 'append_to_file', 'replace_in_file'):
|
|
484
|
+
fname = args.get('filename')
|
|
485
|
+
if fname:
|
|
486
|
+
created_files.add(fname)
|
|
487
|
+
trace.was_successful = True
|
|
488
|
+
outcomes.append(str(res.get('result', ''))[:200])
|
|
489
|
+
|
|
490
|
+
ui_state['log'].append(f" \033[36mTools: {', '.join(tool_names)}\033[0m")
|
|
491
|
+
all_actions.append(", ".join([f"{r.get('tool_name')}({r.get('arguments', {})})" for r in tool_results]))
|
|
492
|
+
all_outcomes.extend(outcomes)
|
|
493
|
+
elif micro_step > 0 and not response.get('tool_calls'):
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
fs_after = get_filesystem_state()
|
|
497
|
+
new_files = set(fs_after.keys()) - set(fs_before.keys())
|
|
498
|
+
if new_files:
|
|
499
|
+
ui_state['log'].append(f" \033[32mNew files: {list(new_files)}\033[0m")
|
|
500
|
+
|
|
501
|
+
combined_thought = " ".join(all_thoughts)
|
|
502
|
+
combined_action = " | ".join(filter(None, all_actions))
|
|
503
|
+
combined_outcome = " | ".join(filter(None, all_outcomes))
|
|
504
|
+
|
|
505
|
+
ui_state['log'].append(f" \033[90mCompressing step...\033[0m")
|
|
506
|
+
summary = summarize_step(
|
|
507
|
+
combined_thought, combined_action, combined_outcome,
|
|
508
|
+
fs_before, fs_after, file_provenance,
|
|
509
|
+
major_step + 1, _model, _provider, agent
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
summary_text = summary.get('summary', 'Step completed.')
|
|
513
|
+
next_text = summary.get('next_step', '')
|
|
514
|
+
ui_state['log'].append(f" \033[32mSummary: {str(summary_text)[:120]}\033[0m")
|
|
515
|
+
if next_text:
|
|
516
|
+
ui_state['log'].append(f" \033[33mNext: {str(next_text)[:100]}\033[0m")
|
|
517
|
+
|
|
518
|
+
summarized_history.append(f"Step {major_step + 1}: {summary_text}")
|
|
519
|
+
trace.steps.append(ResearchStep(
|
|
520
|
+
step=major_step + 1,
|
|
521
|
+
thought=combined_thought,
|
|
522
|
+
action=combined_action,
|
|
523
|
+
outcome=combined_outcome
|
|
524
|
+
))
|
|
525
|
+
|
|
526
|
+
if combined_thought and "RESEARCH_COMPLETE" in combined_thought.upper():
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
major_step += 1
|
|
530
|
+
|
|
531
|
+
for filename in created_files:
|
|
532
|
+
if os.path.exists(filename):
|
|
533
|
+
try:
|
|
534
|
+
trace.final_files[filename] = read_file(filename)
|
|
535
|
+
except:
|
|
536
|
+
pass
|
|
537
|
+
|
|
538
|
+
return trace
|
|
539
|
+
|
|
540
|
+
# ========== Paper Writing (matching original) ==========
|
|
541
|
+
def write_paper(all_traces, user_query, _model, _provider, coordinator, ui_state):
|
|
542
|
+
"""Iterative LaTeX paper building from compressed traces."""
|
|
543
|
+
ui_state['log'].append(f"\n\033[1;36m--- Writing Paper ---\033[0m")
|
|
544
|
+
|
|
545
|
+
# Compress traces for synthesis
|
|
546
|
+
compressed_summaries = []
|
|
547
|
+
for trace in all_traces:
|
|
548
|
+
steps_summary = []
|
|
549
|
+
for step in trace.steps[-3:]:
|
|
550
|
+
thought_short = (step.thought[:100] + "...") if step.thought and len(step.thought) > 100 else (step.thought or "No thought")
|
|
551
|
+
action_short = (step.action[:100] + "...") if step.action and len(step.action) > 100 else (step.action or "No action")
|
|
552
|
+
steps_summary.append(f"Step {step.step}: {thought_short} | {action_short}")
|
|
553
|
+
compressed_summaries.append({
|
|
554
|
+
"agent": trace.agent_name,
|
|
555
|
+
"hypothesis": trace.hypothesis,
|
|
556
|
+
"success": trace.was_successful,
|
|
557
|
+
"key_steps": steps_summary,
|
|
558
|
+
"files_created": list(trace.final_files.keys()),
|
|
559
|
+
})
|
|
560
|
+
compressed_research = json.dumps(compressed_summaries, indent=2)
|
|
561
|
+
|
|
562
|
+
author_list = [trace.agent_name for trace in all_traces]
|
|
563
|
+
author_string = ", ".join(author_list)
|
|
564
|
+
|
|
565
|
+
pct = chr(37) # percent sign - avoid bare % in YAML
|
|
566
|
+
todo = lambda s: f"{pct} TODO: {s}"
|
|
567
|
+
initial_latex = (
|
|
568
|
+
"\\documentclass{article}\n"
|
|
569
|
+
"\\title{" + todo("TITLE") + "}\n"
|
|
570
|
+
"\\author{" + author_string + "}\n"
|
|
571
|
+
"\\date{\\today}\n"
|
|
572
|
+
"\\begin{document}\n"
|
|
573
|
+
"\\maketitle\n\n"
|
|
574
|
+
"\\begin{abstract}\n"
|
|
575
|
+
+ todo("ABSTRACT") + "\n"
|
|
576
|
+
"\\end{abstract}\n\n"
|
|
577
|
+
"\\section{Introduction}\n"
|
|
578
|
+
+ todo("INTRODUCTION") + "\n\n"
|
|
579
|
+
"\\section{Methods}\n"
|
|
580
|
+
+ todo("METHODS") + "\n\n"
|
|
581
|
+
"\\section{Results}\n"
|
|
582
|
+
+ todo("RESULTS") + "\n\n"
|
|
583
|
+
"\\section{Discussion}\n"
|
|
584
|
+
+ todo("DISCUSSION") + "\n\n"
|
|
585
|
+
"\\end{document}"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
create_file("paper.tex", initial_latex)
|
|
589
|
+
ui_state['log'].append(f" Created paper.tex scaffold")
|
|
590
|
+
|
|
591
|
+
todo_sections = ["TITLE", "ABSTRACT", "INTRODUCTION", "METHODS", "RESULTS", "DISCUSSION"]
|
|
592
|
+
|
|
593
|
+
for section_round in range(len(todo_sections)):
|
|
594
|
+
current_paper = read_file("paper.tex")
|
|
595
|
+
sections_status = {s: "EMPTY" if (chr(37) + " TODO: " + s) in current_paper else "COMPLETE" for s in todo_sections}
|
|
596
|
+
|
|
597
|
+
next_section = None
|
|
598
|
+
for s in todo_sections:
|
|
599
|
+
if sections_status[s] == "EMPTY":
|
|
600
|
+
next_section = s
|
|
601
|
+
break
|
|
602
|
+
if not next_section:
|
|
603
|
+
ui_state['log'].append(f" \033[32mAll sections complete\033[0m")
|
|
604
|
+
break
|
|
605
|
+
|
|
606
|
+
ui_state['log'].append(f" Writing section: {next_section}")
|
|
607
|
+
|
|
608
|
+
coord_messages = []
|
|
609
|
+
section_prompt = f"""You are writing a research paper about: "{user_query}"
|
|
610
|
+
|
|
611
|
+
Research data from sub-agents: {compressed_research[:3000]}
|
|
612
|
+
|
|
613
|
+
Current paper content:
|
|
614
|
+
{current_paper}
|
|
615
|
+
|
|
616
|
+
Your task: Complete the {next_section} section by replacing the "{chr(37)} TODO: {next_section}" marker with actual content.
|
|
617
|
+
|
|
618
|
+
Use replace_in_file to update the paper. Use _web_search_tool if you need more information.
|
|
619
|
+
|
|
620
|
+
Focus ONLY on the {next_section} section. Write 2-4 paragraphs of substantial academic content.
|
|
621
|
+
|
|
622
|
+
Available tools: replace_in_file, read_file, _web_search_tool"""
|
|
623
|
+
|
|
624
|
+
for micro in range(5):
|
|
625
|
+
if micro == 0:
|
|
626
|
+
cprompt = section_prompt
|
|
627
|
+
else:
|
|
628
|
+
cprompt = f"Continue working on the {next_section} section. What's your next action?"
|
|
629
|
+
try:
|
|
630
|
+
resp = coordinator.get_llm_response(
|
|
631
|
+
cprompt, messages=coord_messages, auto_process_tool_calls=True
|
|
632
|
+
)
|
|
633
|
+
coord_messages = resp.get('messages', [])
|
|
634
|
+
if resp.get('tool_results'):
|
|
635
|
+
for tr in resp['tool_results']:
|
|
636
|
+
ui_state['log'].append(f" \033[36m{tr.get('tool_name', '?')}\033[0m")
|
|
637
|
+
except:
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
final_paper = read_file("paper.tex")
|
|
641
|
+
return final_paper
|
|
642
|
+
|
|
643
|
+
# ========== Sub-Agent Review Cycle ==========
|
|
644
|
+
def do_sub_agent_reviews(all_traces, paper_content, user_query, _model, _provider, cycle_num, ui_state):
|
|
645
|
+
"""Each sub-agent reviews the paper and provides suggestions."""
|
|
646
|
+
ui_state['log'].append(f"\n\033[1;35m--- Cycle {cycle_num}: Sub-Agent Reviews ---\033[0m")
|
|
647
|
+
suggestions = []
|
|
648
|
+
|
|
649
|
+
for i, trace in enumerate(all_traces):
|
|
650
|
+
if ui_state.get('skip'):
|
|
651
|
+
ui_state['skip'] = False
|
|
652
|
+
break
|
|
653
|
+
while ui_state.get('paused'):
|
|
654
|
+
time.sleep(0.2)
|
|
655
|
+
if ui_state.get('skip'):
|
|
656
|
+
break
|
|
657
|
+
|
|
658
|
+
agent_name = trace.agent_name
|
|
659
|
+
ui_state['log'].append(f"\n\033[36m Reviewer: {agent_name}\033[0m")
|
|
660
|
+
|
|
661
|
+
# Build context of what this agent found
|
|
662
|
+
agent_findings = []
|
|
663
|
+
for step in trace.steps:
|
|
664
|
+
if step.thought:
|
|
665
|
+
agent_findings.append(f"Step {step.step}: {step.thought[:300]}")
|
|
666
|
+
findings_text = "\n".join(agent_findings[-5:])
|
|
667
|
+
files_text = ", ".join(trace.final_files.keys()) if trace.final_files else "(none)"
|
|
668
|
+
|
|
669
|
+
review_prompt = f"""You are {agent_name}, a research sub-agent. You previously investigated the hypothesis:
|
|
670
|
+
"{trace.hypothesis}"
|
|
671
|
+
|
|
672
|
+
Your key findings were:
|
|
673
|
+
{findings_text}
|
|
674
|
+
|
|
675
|
+
Files you created: {files_text}
|
|
676
|
+
|
|
677
|
+
Now review the following research paper written about the query: "{user_query}"
|
|
678
|
+
|
|
679
|
+
=== PAPER ===
|
|
680
|
+
{paper_content[:4000]}
|
|
681
|
+
=== END PAPER ===
|
|
682
|
+
|
|
683
|
+
Based on your expertise and findings, provide a critical review with specific suggestions:
|
|
684
|
+
1. Are your findings accurately represented?
|
|
685
|
+
2. What important findings or nuances are missing?
|
|
686
|
+
3. What claims need stronger evidence or qualification?
|
|
687
|
+
4. What additional analysis or experiments should be done?
|
|
688
|
+
5. Specific text improvements (cite sections by name).
|
|
193
689
|
|
|
194
|
-
|
|
195
|
-
model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else 'gemini-1.5-pro')
|
|
196
|
-
provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else 'gemini')
|
|
690
|
+
Be concrete and specific. Reference sections (Introduction, Methods, Results, Discussion) directly."""
|
|
197
691
|
|
|
198
|
-
|
|
199
|
-
|
|
692
|
+
try:
|
|
693
|
+
resp = get_llm_response(review_prompt, model=_model, provider=_provider, npc=None)
|
|
694
|
+
review_text = resp.get('response', '')
|
|
695
|
+
if review_text:
|
|
696
|
+
suggestions.append({
|
|
697
|
+
'agent': agent_name,
|
|
698
|
+
'hypothesis': trace.hypothesis,
|
|
699
|
+
'review': review_text,
|
|
700
|
+
})
|
|
701
|
+
preview = review_text.replace('\n', ' ')[:120]
|
|
702
|
+
ui_state['log'].append(f" {preview}")
|
|
703
|
+
else:
|
|
704
|
+
ui_state['log'].append(f" \033[90m(empty review)\033[0m")
|
|
705
|
+
except Exception as e:
|
|
706
|
+
ui_state['log'].append(f" \033[31mReview error: {str(e)[:80]}\033[0m")
|
|
707
|
+
|
|
708
|
+
return suggestions
|
|
709
|
+
|
|
710
|
+
def revise_paper(paper_content, suggestions, user_query, coordinator, cycle_num, ui_state):
|
|
711
|
+
"""Coordinator revises the paper based on sub-agent suggestions."""
|
|
712
|
+
ui_state['log'].append(f"\n\033[1;36m--- Cycle {cycle_num}: Revising Paper ---\033[0m")
|
|
713
|
+
|
|
714
|
+
# Format all suggestions
|
|
715
|
+
suggestions_text = ""
|
|
716
|
+
for s in suggestions:
|
|
717
|
+
suggestions_text += f"\n--- Review by {s['agent']} (Hypothesis: {s['hypothesis'][:80]}) ---\n"
|
|
718
|
+
suggestions_text += s['review'][:1500] + "\n"
|
|
719
|
+
|
|
720
|
+
# Write current paper to file for coordinator to edit
|
|
721
|
+
paper_path = os.path.abspath("paper.tex")
|
|
722
|
+
with open(paper_path, 'w') as f:
|
|
723
|
+
f.write(paper_content)
|
|
724
|
+
|
|
725
|
+
revision_prompt = f"""You are revising a research paper about: "{user_query}"
|
|
726
|
+
|
|
727
|
+
This is revision cycle {cycle_num}. Your sub-agents have reviewed the paper and provided suggestions.
|
|
728
|
+
|
|
729
|
+
CURRENT PAPER:
|
|
730
|
+
{paper_content[:4000]}
|
|
731
|
+
|
|
732
|
+
SUB-AGENT REVIEWS AND SUGGESTIONS:
|
|
733
|
+
{suggestions_text[:4000]}
|
|
734
|
+
|
|
735
|
+
Your task:
|
|
736
|
+
1. Read the reviews carefully
|
|
737
|
+
2. Incorporate valid suggestions using replace_in_file on paper.tex
|
|
738
|
+
3. Strengthen weak sections
|
|
739
|
+
4. Add missing findings or nuances
|
|
740
|
+
5. Improve clarity and academic rigor
|
|
741
|
+
6. Do NOT remove existing good content - only improve and add
|
|
742
|
+
|
|
743
|
+
Use replace_in_file to make targeted improvements to paper.tex.
|
|
744
|
+
Use read_file to check current state.
|
|
745
|
+
|
|
746
|
+
Available tools: replace_in_file, read_file, append_to_file, _web_search_tool"""
|
|
747
|
+
|
|
748
|
+
coord_messages = []
|
|
749
|
+
for micro in range(8):
|
|
750
|
+
if ui_state.get('skip'):
|
|
751
|
+
break
|
|
752
|
+
if micro == 0:
|
|
753
|
+
cprompt = revision_prompt
|
|
754
|
+
else:
|
|
755
|
+
cprompt = "Continue revising the paper. What's your next improvement?"
|
|
756
|
+
try:
|
|
757
|
+
resp = coordinator.get_llm_response(
|
|
758
|
+
cprompt, messages=coord_messages, auto_process_tool_calls=True
|
|
759
|
+
)
|
|
760
|
+
coord_messages = resp.get('messages', [])
|
|
761
|
+
response_text = resp.get('response', '')
|
|
762
|
+
if resp.get('tool_results'):
|
|
763
|
+
for tr in resp['tool_results']:
|
|
764
|
+
ui_state['log'].append(f" \033[36m{tr.get('tool_name', '?')}\033[0m")
|
|
765
|
+
elif micro > 0 and not resp.get('tool_calls'):
|
|
766
|
+
break
|
|
767
|
+
except Exception as e:
|
|
768
|
+
ui_state['log'].append(f" \033[31mRevision error: {str(e)[:80]}\033[0m")
|
|
769
|
+
break
|
|
770
|
+
|
|
771
|
+
revised = read_file("paper.tex")
|
|
772
|
+
ui_state['log'].append(f" \033[32mRevision cycle {cycle_num} complete\033[0m")
|
|
773
|
+
return revised
|
|
774
|
+
|
|
775
|
+
# ========== Trace Saving (matching original) ==========
|
|
776
|
+
def save_traces_csv(all_traces):
|
|
777
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
778
|
+
trace_dir = os.path.join(_work_dir, "alicanto_traces")
|
|
779
|
+
os.makedirs(trace_dir, exist_ok=True)
|
|
780
|
+
filepath = os.path.join(trace_dir, f"trace_{timestamp}.csv")
|
|
781
|
+
rows = []
|
|
782
|
+
for trace in all_traces:
|
|
783
|
+
for step in trace.steps:
|
|
784
|
+
rows.append({
|
|
785
|
+
"hypothesis": trace.hypothesis,
|
|
786
|
+
"agent_name": trace.agent_name,
|
|
787
|
+
"agent_persona": trace.agent_persona,
|
|
788
|
+
"was_successful": trace.was_successful,
|
|
789
|
+
"step": step.step,
|
|
790
|
+
"thought": step.thought[:2000],
|
|
791
|
+
"action": step.action[:1000],
|
|
792
|
+
"outcome": step.outcome[:1000],
|
|
793
|
+
"final_files": json.dumps(list(trace.final_files.keys())),
|
|
794
|
+
})
|
|
795
|
+
if rows:
|
|
796
|
+
with open(filepath, 'w', newline='') as f:
|
|
797
|
+
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
|
|
798
|
+
writer.writeheader()
|
|
799
|
+
writer.writerows(rows)
|
|
800
|
+
return filepath
|
|
801
|
+
|
|
802
|
+
# ========== Global State ==========
|
|
803
|
+
class AlicantoState:
|
|
804
|
+
def __init__(self):
|
|
805
|
+
self.phase = 0 # 0=query, 1=hypotheses, 2=personas, 3=execution, 4=review
|
|
806
|
+
self.auto_mode = False
|
|
807
|
+
self.query = query or ""
|
|
808
|
+
self.hypotheses = [] # list of hypothesis strings
|
|
809
|
+
self.personas = [] # list of persona dicts
|
|
810
|
+
self.traces = [] # list of SubAgentTrace
|
|
811
|
+
self.current_agent = -1
|
|
812
|
+
# UI state
|
|
813
|
+
self.sel = 0
|
|
814
|
+
self.scroll = 0
|
|
815
|
+
self.mode = 'normal' # normal, edit
|
|
816
|
+
self.input_buf = ""
|
|
817
|
+
self.input_target = ""
|
|
818
|
+
self.status = ""
|
|
819
|
+
self.generating = False
|
|
820
|
+
self.error = ""
|
|
821
|
+
# Review state
|
|
822
|
+
self.review_tab = 0 # 0=agents, 1=insights, 2=files, 3=paper
|
|
823
|
+
self.starred = set()
|
|
824
|
+
self.paper = ""
|
|
825
|
+
self.all_insights = []
|
|
826
|
+
self.all_files = [] # [{name, agent, content}]
|
|
827
|
+
# Execution display
|
|
828
|
+
self.exec_log = []
|
|
829
|
+
self.exec_scroll = 0
|
|
830
|
+
self.exec_paused = False
|
|
831
|
+
self.exec_skip = False
|
|
832
|
+
self.current_cycle = 0
|
|
833
|
+
self.total_cycles = num_cycles
|
|
834
|
+
self.end_cycles = False
|
|
835
|
+
# Shared state for sub-agent communication
|
|
836
|
+
self.agent_ui = {'log': [], 'skip': False, 'paused': False}
|
|
837
|
+
|
|
838
|
+
@property
|
|
839
|
+
def phase_name(self):
|
|
840
|
+
return ["Query", "Hypotheses", "Personas", "Execution", "Review"][self.phase]
|
|
841
|
+
|
|
842
|
+
ui = AlicantoState()
|
|
843
|
+
|
|
844
|
+
# ========== Query Entry ==========
|
|
845
|
+
if not ui.query:
|
|
846
|
+
if sys.stdin.isatty():
|
|
847
|
+
print("\033[1;36m ALICANTO - Multi-Agent Research System \033[0m")
|
|
848
|
+
print("\033[90mEnter your research query (or 'q' to quit):\033[0m")
|
|
849
|
+
try:
|
|
850
|
+
ui.query = input("\033[33m> \033[0m").strip()
|
|
851
|
+
except (EOFError, KeyboardInterrupt):
|
|
852
|
+
ui.query = ""
|
|
853
|
+
if not ui.query or ui.query.lower() == 'q':
|
|
854
|
+
os.chdir(_orig_cwd)
|
|
855
|
+
context['output'] = "Alicanto cancelled."
|
|
856
|
+
context['messages'] = messages
|
|
857
|
+
exit()
|
|
858
|
+
else:
|
|
859
|
+
os.chdir(_orig_cwd)
|
|
860
|
+
context['output'] = """Usage: /alicanto <research query>
|
|
200
861
|
|
|
201
862
|
Options:
|
|
202
|
-
--num-npcs N
|
|
203
|
-
--
|
|
204
|
-
--
|
|
205
|
-
--
|
|
206
|
-
--
|
|
207
|
-
--format FORMAT Output: report|summary|full (default: report)
|
|
863
|
+
--num-npcs N Number of sub-agents (default: 3)
|
|
864
|
+
--max-steps N Max major steps per agent (default: 10)
|
|
865
|
+
--num-cycles N Review/revision cycles (default: 3)
|
|
866
|
+
--model MODEL LLM model
|
|
867
|
+
--provider PROVIDER LLM provider
|
|
208
868
|
|
|
209
869
|
Example: /alicanto What are the latest advances in quantum computing?"""
|
|
210
|
-
|
|
211
|
-
|
|
870
|
+
context['messages'] = messages
|
|
871
|
+
exit()
|
|
212
872
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
██║ ██║███████╗██║╚██████╗██║ ██║██║ ╚████║ ██║ ╚██████╔╝
|
|
219
|
-
╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝
|
|
220
|
-
|
|
221
|
-
Deep Research Mode
|
|
222
|
-
Query: {query}
|
|
223
|
-
Perspectives: {num_npcs} | Depth: {depth} | Max Steps: {max_steps}
|
|
224
|
-
""")
|
|
225
|
-
|
|
226
|
-
# Generate research perspectives
|
|
227
|
-
perspectives_prompt = f"""Generate {num_npcs} distinct research perspectives for investigating: "{query}"
|
|
228
|
-
|
|
229
|
-
For each perspective, provide:
|
|
230
|
-
1. Name (a descriptive title)
|
|
231
|
-
2. Approach (how this perspective would investigate)
|
|
232
|
-
3. Key questions to explore
|
|
233
|
-
|
|
234
|
-
Return as a numbered list."""
|
|
235
|
-
|
|
236
|
-
print(colored("Generating research perspectives...", "cyan"))
|
|
237
|
-
resp = get_llm_response(
|
|
238
|
-
perspectives_prompt,
|
|
239
|
-
model=model,
|
|
240
|
-
provider=provider,
|
|
241
|
-
npc=npc
|
|
242
|
-
)
|
|
243
|
-
perspectives = str(resp.get('response', ''))
|
|
244
|
-
print(perspectives)
|
|
245
|
-
|
|
246
|
-
# Conduct web research if not skipped
|
|
247
|
-
research_findings = ""
|
|
248
|
-
if not skip_research:
|
|
249
|
-
print(colored("\nConducting web research...", "cyan"))
|
|
873
|
+
# ========== Generation Functions ==========
|
|
874
|
+
def do_generate_hypotheses():
|
|
875
|
+
ui.status = "Generating hypotheses..."
|
|
876
|
+
ui.generating = True
|
|
877
|
+
ui.error = ""
|
|
250
878
|
try:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
879
|
+
one_shot = '''
|
|
880
|
+
"example_input": "Investigate the impact of quantum annealing on protein folding.",
|
|
881
|
+
"example_output": {
|
|
882
|
+
"hypotheses": [
|
|
883
|
+
"Implementing a quantum annealer simulation for a small peptide chain will identify lower energy states faster than a classical simulated annealing approach.",
|
|
884
|
+
"The choice of qubit connectivity in the quantum annealer topology significantly impacts the final folded state accuracy for proteins with long-range interactions.",
|
|
885
|
+
"Encoding the protein residue interactions as a QUBO problem is feasible for structures up to 50 amino acids before qubit requirements become prohibitive."
|
|
886
|
+
]
|
|
887
|
+
}'''
|
|
888
|
+
prompt = f"""Based on the following research topic, generate a list of {num_npcs} distinct, specific, and empirically testable hypotheses.
|
|
261
889
|
|
|
262
|
-
|
|
263
|
-
all_insights = []
|
|
264
|
-
gold_insights = [] # Key valuable findings
|
|
265
|
-
cliff_warnings = [] # Potential pitfalls or caveats
|
|
890
|
+
TOPIC: "{ui.query}"
|
|
266
891
|
|
|
267
|
-
|
|
268
|
-
print(colored(f"\n--- Research Depth {step + 1}/{depth} ---", "cyan"))
|
|
892
|
+
Return a JSON object with a single key "hypotheses" which is a list of strings.
|
|
269
893
|
|
|
270
|
-
|
|
894
|
+
Here is an example of the expected input and output format:
|
|
895
|
+
{one_shot}
|
|
271
896
|
|
|
272
|
-
|
|
273
|
-
{perspectives}
|
|
897
|
+
Return ONLY the JSON object."""
|
|
274
898
|
|
|
275
|
-
|
|
899
|
+
resp = get_llm_response(prompt, model=model, provider=provider, npc=npc, format='json')
|
|
900
|
+
result = resp.get('response')
|
|
901
|
+
if isinstance(result, str):
|
|
902
|
+
raw = result
|
|
903
|
+
start = raw.find('{')
|
|
904
|
+
end = raw.rfind('}') + 1
|
|
905
|
+
if start >= 0 and end > start:
|
|
906
|
+
raw = raw[start:end]
|
|
907
|
+
result = json.loads(raw)
|
|
908
|
+
if isinstance(result, dict):
|
|
909
|
+
ui.hypotheses = result.get('hypotheses', [])
|
|
910
|
+
elif isinstance(result, list):
|
|
911
|
+
ui.hypotheses = result
|
|
912
|
+
else:
|
|
913
|
+
ui.hypotheses = [str(result)]
|
|
914
|
+
|
|
915
|
+
if not ui.hypotheses:
|
|
916
|
+
ui.hypotheses = [f"Investigate: {ui.query}"]
|
|
917
|
+
except Exception as e:
|
|
918
|
+
ui.error = f"Hypothesis error: {e}"
|
|
919
|
+
ui.hypotheses = [f"Investigate: {ui.query}"]
|
|
920
|
+
ui.generating = False
|
|
921
|
+
ui.status = f"{len(ui.hypotheses)} hypotheses generated"
|
|
276
922
|
|
|
277
|
-
|
|
923
|
+
def do_generate_personas():
|
|
924
|
+
ui.status = "Generating personas..."
|
|
925
|
+
ui.generating = True
|
|
926
|
+
ui.error = ""
|
|
927
|
+
n = len(ui.hypotheses)
|
|
928
|
+
ui.personas = []
|
|
929
|
+
for i in range(n):
|
|
930
|
+
birth_year = random.randint(-32665, 32665)
|
|
931
|
+
ui.status = f"Generating persona {i+1}/{n} (born {birth_year})..."
|
|
932
|
+
try:
|
|
933
|
+
persona = generate_one_persona(birth_year, model, provider, npc)
|
|
934
|
+
persona['hypothesis_idx'] = i
|
|
935
|
+
ui.personas.append(persona)
|
|
936
|
+
except Exception as e:
|
|
937
|
+
ui.personas.append({
|
|
938
|
+
'name': f'Agent {i+1}', 'birth_year': birth_year,
|
|
939
|
+
'location': 'Unknown', 'leader': 'Unknown',
|
|
940
|
+
'interests': [], 'worldview': '', 'approach': '',
|
|
941
|
+
'persona_text': f'You are Agent {i+1}, born {birth_year}.',
|
|
942
|
+
'hypothesis_idx': i,
|
|
943
|
+
})
|
|
944
|
+
ui.error = f"Persona {i+1} error: {e}"
|
|
945
|
+
ui.generating = False
|
|
946
|
+
ui.status = f"{len(ui.personas)} personas generated"
|
|
278
947
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
948
|
+
def do_run_all_agents():
|
|
949
|
+
"""Run all sub-agents serially, write paper, then iterate review cycles."""
|
|
950
|
+
ui.generating = True
|
|
951
|
+
ui.error = ""
|
|
952
|
+
ui.traces = []
|
|
953
|
+
ui.agent_ui = {'log': ui.exec_log, 'skip': False, 'paused': False}
|
|
284
954
|
|
|
285
|
-
|
|
286
|
-
|
|
955
|
+
# Create coordinator NPC for paper writing
|
|
956
|
+
def wander_wrapper_coord(problem_description: str) -> str:
|
|
957
|
+
"""Get creative ideas when stuck."""
|
|
958
|
+
if not WANDER_AVAILABLE:
|
|
959
|
+
return "Wander not available."
|
|
960
|
+
try:
|
|
961
|
+
_, _, raw, _, _ = perform_single_wandering(
|
|
962
|
+
problem=problem_description, npc=coordinator, model=model, provider=provider
|
|
963
|
+
)
|
|
964
|
+
return str(raw)
|
|
965
|
+
except Exception as e:
|
|
966
|
+
return f"Wander failed: {e}"
|
|
287
967
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
968
|
+
coord_tools = [create_file, append_to_file, replace_in_file, read_file,
|
|
969
|
+
list_files, execute_shell_command, _web_search_tool, wander_wrapper_coord]
|
|
970
|
+
|
|
971
|
+
coordinator = NPC(
|
|
972
|
+
name="Alicanto",
|
|
973
|
+
model=model, provider=provider,
|
|
974
|
+
primary_directive="You are Alicanto the mythical bird. You research topics iteratively by writing to LaTeX files and searching for more information.",
|
|
975
|
+
tools=coord_tools
|
|
294
976
|
)
|
|
295
977
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
gold_insights.extend([line.strip() for line in step_insights.split('\n') if '[GOLD]' in line])
|
|
302
|
-
if '[CLIFF]' in step_insights:
|
|
303
|
-
cliff_warnings.extend([line.strip() for line in step_insights.split('\n') if '[CLIFF]' in line])
|
|
304
|
-
|
|
305
|
-
all_insights.append(step_insights)
|
|
306
|
-
|
|
307
|
-
# Generate final synthesis
|
|
308
|
-
print(colored("\n--- Synthesizing Research ---", "cyan"))
|
|
309
|
-
|
|
310
|
-
synthesis_prompt = f"""Synthesize research on: "{query}"
|
|
311
|
-
|
|
312
|
-
All insights gathered:
|
|
313
|
-
{chr(10).join(all_insights)}
|
|
314
|
-
|
|
315
|
-
Gold insights identified:
|
|
316
|
-
{chr(10).join(gold_insights) if gold_insights else 'None explicitly marked'}
|
|
317
|
-
|
|
318
|
-
Cliff warnings identified:
|
|
319
|
-
{chr(10).join(cliff_warnings) if cliff_warnings else 'None explicitly marked'}
|
|
320
|
-
|
|
321
|
-
Generate a {output_format} that:
|
|
322
|
-
1. Summarizes key findings
|
|
323
|
-
2. Highlights the most valuable insights (gold)
|
|
324
|
-
3. Notes important caveats and risks (cliffs)
|
|
325
|
-
4. Provides actionable conclusions"""
|
|
326
|
-
|
|
327
|
-
resp = get_llm_response(
|
|
328
|
-
synthesis_prompt,
|
|
329
|
-
model=model,
|
|
330
|
-
provider=provider,
|
|
331
|
-
npc=npc
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
final_report = str(resp.get('response', ''))
|
|
335
|
-
print("\n" + "="*60)
|
|
336
|
-
print(colored("ALICANTO RESEARCH REPORT", "green", attrs=['bold']))
|
|
337
|
-
print("="*60)
|
|
338
|
-
print(final_report)
|
|
339
|
-
|
|
340
|
-
alicanto_result = {
|
|
341
|
-
'query': query,
|
|
342
|
-
'perspectives': perspectives,
|
|
343
|
-
'insights': all_insights,
|
|
344
|
-
'gold': gold_insights,
|
|
345
|
-
'cliffs': cliff_warnings,
|
|
346
|
-
'report': final_report
|
|
347
|
-
}
|
|
978
|
+
# ===== Cycle 1: Sub-agent experimentation =====
|
|
979
|
+
ui.current_cycle = 1
|
|
980
|
+
ui.exec_log.append(f"\n\033[1;33m{'='*50}\033[0m")
|
|
981
|
+
ui.exec_log.append(f"\033[1;33m CYCLE 1/{num_cycles}: Sub-Agent Experimentation\033[0m")
|
|
982
|
+
ui.exec_log.append(f"\033[1;33m{'='*50}\033[0m")
|
|
348
983
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
984
|
+
for i in range(len(ui.hypotheses)):
|
|
985
|
+
hyp = ui.hypotheses[i]
|
|
986
|
+
persona = ui.personas[i % len(ui.personas)]
|
|
987
|
+
ui.current_agent = i
|
|
988
|
+
ui.exec_log.append(f"\n\033[1;36m--- Agent {i+1}/{len(ui.hypotheses)}: {persona['name']} ---\033[0m")
|
|
989
|
+
ui.exec_log.append(f" \033[90mHypothesis: {hyp[:100]}\033[0m")
|
|
990
|
+
ui.exec_log.append(f" \033[90mBorn {persona['birth_year']} in {persona.get('location', '?')}\033[0m")
|
|
991
|
+
|
|
992
|
+
ui.agent_ui['skip'] = ui.exec_skip
|
|
993
|
+
ui.agent_ui['paused'] = ui.exec_paused
|
|
994
|
+
|
|
995
|
+
trace = run_sub_agent_trace(hyp, persona, ui.query, model, provider, max_steps, ui.agent_ui)
|
|
996
|
+
ui.traces.append(trace)
|
|
997
|
+
ui.exec_log.append(f" \033[1;32mCompleted: {'SUCCESS' if trace.was_successful else 'no files'}, {len(trace.steps)} steps\033[0m")
|
|
998
|
+
|
|
999
|
+
# Save traces
|
|
1000
|
+
try:
|
|
1001
|
+
trace_path = save_traces_csv(ui.traces)
|
|
1002
|
+
ui.exec_log.append(f"\n\033[90mTraces saved: {trace_path}\033[0m")
|
|
1003
|
+
except Exception as e:
|
|
1004
|
+
ui.exec_log.append(f"\n\033[31mTrace save error: {e}\033[0m")
|
|
1005
|
+
|
|
1006
|
+
# ===== Cycle 1: Coordinator writes initial paper =====
|
|
1007
|
+
ui.exec_log.append(f"\n\033[1;36m--- Coordinator: Writing Initial Paper ---\033[0m")
|
|
1008
|
+
try:
|
|
1009
|
+
ui.paper = write_paper(ui.traces, ui.query, model, provider, coordinator, ui.agent_ui)
|
|
1010
|
+
except Exception as e:
|
|
1011
|
+
ui.paper = f"Paper writing error: {e}"
|
|
1012
|
+
ui.exec_log.append(f" \033[31m{e}\033[0m")
|
|
1013
|
+
|
|
1014
|
+
# ===== Cycles 2..N: Review and revise =====
|
|
1015
|
+
for cycle in range(2, num_cycles + 1):
|
|
1016
|
+
if ui.end_cycles:
|
|
1017
|
+
ui.exec_log.append(f"\n\033[33mCycles ended early by user at cycle {cycle-1}\033[0m")
|
|
1018
|
+
break
|
|
1019
|
+
|
|
1020
|
+
ui.current_cycle = cycle
|
|
1021
|
+
ui.exec_log.append(f"\n\033[1;33m{'='*50}\033[0m")
|
|
1022
|
+
ui.exec_log.append(f"\033[1;33m CYCLE {cycle}/{num_cycles}: Review & Revise\033[0m")
|
|
1023
|
+
ui.exec_log.append(f"\033[1;33m{'='*50}\033[0m")
|
|
1024
|
+
|
|
1025
|
+
# Sub-agents review the paper
|
|
1026
|
+
suggestions = do_sub_agent_reviews(
|
|
1027
|
+
ui.traces, ui.paper, ui.query, model, provider, cycle, ui.agent_ui
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
if not suggestions:
|
|
1031
|
+
ui.exec_log.append(f"\n\033[90mNo suggestions received, skipping revision\033[0m")
|
|
1032
|
+
continue
|
|
1033
|
+
|
|
1034
|
+
if ui.end_cycles:
|
|
1035
|
+
break
|
|
1036
|
+
|
|
1037
|
+
# Coordinator revises based on suggestions
|
|
1038
|
+
ui.paper = revise_paper(
|
|
1039
|
+
ui.paper, suggestions, ui.query, coordinator, cycle, ui.agent_ui
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
ui.exec_log.append(f"\n\033[1;32m{'='*50}\033[0m")
|
|
1043
|
+
ui.exec_log.append(f"\033[1;32m All {ui.current_cycle} cycle(s) complete\033[0m")
|
|
1044
|
+
ui.exec_log.append(f"\033[1;32m{'='*50}\033[0m")
|
|
1045
|
+
|
|
1046
|
+
# Collect insights and files
|
|
1047
|
+
ui.all_insights = []
|
|
1048
|
+
ui.all_files = []
|
|
1049
|
+
for trace in ui.traces:
|
|
1050
|
+
for step in trace.steps:
|
|
1051
|
+
if step.thought:
|
|
1052
|
+
ui.all_insights.append(f"[{trace.agent_name}] Step {step.step}: {step.thought[:300]}")
|
|
1053
|
+
for fname, content in trace.final_files.items():
|
|
1054
|
+
ui.all_files.append({"name": fname, "agent": trace.agent_name, "content": content[:1000]})
|
|
1055
|
+
|
|
1056
|
+
ui.generating = False
|
|
1057
|
+
ui.phase = 4
|
|
1058
|
+
ui.sel = 0
|
|
1059
|
+
ui.scroll = 0
|
|
1060
|
+
ui.status = f"Research complete ({ui.current_cycle} cycles). Review results."
|
|
1061
|
+
|
|
1062
|
+
# ========== TUI Rendering ==========
|
|
1063
|
+
def render_screen():
|
|
1064
|
+
width, height = get_size()
|
|
1065
|
+
out = []
|
|
1066
|
+
out.append("\033[2J\033[H")
|
|
1067
|
+
|
|
1068
|
+
auto_badge = " \033[32m[AUTO]\033[0m" if ui.auto_mode else ""
|
|
1069
|
+
phase_display = f" ALICANTO - Phase {ui.phase+1}: {ui.phase_name} "
|
|
1070
|
+
out.append(f"\033[1;1H\033[7;1m{phase_display.ljust(width)}\033[0m")
|
|
1071
|
+
out.append(f"\033[1;{width-20}H{auto_badge}")
|
|
1072
|
+
|
|
1073
|
+
if ui.phase == 0:
|
|
1074
|
+
render_phase_query(out, width, height)
|
|
1075
|
+
elif ui.phase == 1:
|
|
1076
|
+
render_phase_hypotheses(out, width, height)
|
|
1077
|
+
elif ui.phase == 2:
|
|
1078
|
+
render_phase_personas(out, width, height)
|
|
1079
|
+
elif ui.phase == 3:
|
|
1080
|
+
render_phase_execution(out, width, height)
|
|
1081
|
+
elif ui.phase == 4:
|
|
1082
|
+
render_phase_review(out, width, height)
|
|
1083
|
+
|
|
1084
|
+
status_color = "\033[33m" if ui.generating else "\033[90m"
|
|
1085
|
+
err_text = f" \033[31m{ui.error}\033[0m" if ui.error else ""
|
|
1086
|
+
out.append(f"\033[{height-1};1H\033[K{status_color}{ui.status}\033[0m{err_text}")
|
|
1087
|
+
|
|
1088
|
+
sys.stdout.write(''.join(out))
|
|
1089
|
+
sys.stdout.flush()
|
|
1090
|
+
|
|
1091
|
+
def render_phase_query(out, width, height):
|
|
1092
|
+
banner = [
|
|
1093
|
+
"\033[36m █████╗ ██╗ ██╗ ██████╗ █████╗ ███╗ ██╗████████╗ ██████╗\033[0m",
|
|
1094
|
+
"\033[36m██╔══██╗██║ ██║██╔════╝██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗\033[0m",
|
|
1095
|
+
"\033[36m███████║██║ ██║██║ ███████║██╔██╗ ██║ ██║ ██║ ██║\033[0m",
|
|
1096
|
+
"\033[36m██╔══██║██║ ██║██║ ██╔══██║██║╚██╗██║ ██║ ██║ ██║\033[0m",
|
|
1097
|
+
"\033[36m██║ ██║███████╗██║╚██████╗██║ ██║██║ ╚████║ ██║ ╚██████╔╝\033[0m",
|
|
1098
|
+
"\033[36m╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝\033[0m",
|
|
1099
|
+
]
|
|
1100
|
+
for i, line in enumerate(banner):
|
|
1101
|
+
out.append(f"\033[{3+i};3H{line}")
|
|
1102
|
+
|
|
1103
|
+
y = 3 + len(banner) + 2
|
|
1104
|
+
out.append(f"\033[{y};3H\033[1mQuery:\033[0m {ui.query}")
|
|
1105
|
+
out.append(f"\033[{y+1};3H\033[90mAgents: {num_npcs} | Max steps: {max_steps} | Cycles: {num_cycles} | Model: {model or 'default'} | Provider: {provider or 'default'}\033[0m")
|
|
1106
|
+
out.append(f"\033[{height};1H\033[K\033[7m Enter:Continue A:Auto-mode q:Quit \033[0m".ljust(width))
|
|
1107
|
+
|
|
1108
|
+
def render_phase_hypotheses(out, width, height):
|
|
1109
|
+
list_h = height - 6
|
|
1110
|
+
n = len(ui.hypotheses)
|
|
1111
|
+
|
|
1112
|
+
if ui.mode == 'edit':
|
|
1113
|
+
out.append(f"\033[3;1H\033[33mEditing hypothesis:\033[0m")
|
|
1114
|
+
out.append(f"\033[4;1H\033[7m {ui.input_buf} \033[0m")
|
|
1115
|
+
out.append(f"\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m".ljust(width))
|
|
1116
|
+
return
|
|
1117
|
+
|
|
1118
|
+
out.append(f"\033[3;1H\033[1m Hypotheses ({n}) \033[90m{'─' * (width - 20)}\033[0m")
|
|
1119
|
+
|
|
1120
|
+
if ui.sel < ui.scroll:
|
|
1121
|
+
ui.scroll = ui.sel
|
|
1122
|
+
elif ui.sel >= ui.scroll + (list_h // 2):
|
|
1123
|
+
ui.scroll = ui.sel - (list_h // 2) + 1
|
|
1124
|
+
|
|
1125
|
+
y = 4
|
|
1126
|
+
for i in range(len(ui.hypotheses)):
|
|
1127
|
+
if i < ui.scroll:
|
|
1128
|
+
continue
|
|
1129
|
+
if y >= height - 3:
|
|
1130
|
+
break
|
|
1131
|
+
h = ui.hypotheses[i]
|
|
1132
|
+
selected = (i == ui.sel)
|
|
1133
|
+
prefix = "\033[7m>" if selected else " "
|
|
1134
|
+
out.append(f"\033[{y};1H\033[K{prefix} \033[1mH{i+1}\033[0m: {str(h)[:width-10]}")
|
|
1135
|
+
if selected:
|
|
1136
|
+
out.append("\033[0m")
|
|
1137
|
+
y += 1
|
|
1138
|
+
if selected:
|
|
1139
|
+
for dl in wrap_text(h, width - 6)[1:4]:
|
|
1140
|
+
if y >= height - 3:
|
|
1141
|
+
break
|
|
1142
|
+
out.append(f"\033[{y};5H\033[K\033[90m{dl}\033[0m")
|
|
1143
|
+
y += 1
|
|
1144
|
+
y += 1
|
|
1145
|
+
|
|
1146
|
+
while y < height - 2:
|
|
1147
|
+
out.append(f"\033[{y};1H\033[K")
|
|
1148
|
+
y += 1
|
|
1149
|
+
|
|
1150
|
+
gen = " (generating...)" if ui.generating else ""
|
|
1151
|
+
out.append(f"\033[{height};1H\033[K\033[7m j/k:Nav e:Edit d:Delete +:Add r:Regen Enter:Approve A:Auto q:Quit{gen} \033[0m".ljust(width))
|
|
1152
|
+
|
|
1153
|
+
def render_phase_personas(out, width, height):
|
|
1154
|
+
list_h = height - 6
|
|
1155
|
+
n = len(ui.personas)
|
|
1156
|
+
|
|
1157
|
+
if ui.mode == 'edit':
|
|
1158
|
+
out.append(f"\033[3;1H\033[33mEditing {ui.input_target}:\033[0m")
|
|
1159
|
+
out.append(f"\033[4;1H\033[7m {ui.input_buf} \033[0m")
|
|
1160
|
+
out.append(f"\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m".ljust(width))
|
|
1161
|
+
return
|
|
352
1162
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1163
|
+
out.append(f"\033[3;1H\033[1m Personas ({n}) \033[90m{'─' * (width - 20)}\033[0m")
|
|
1164
|
+
|
|
1165
|
+
if ui.sel < ui.scroll:
|
|
1166
|
+
ui.scroll = ui.sel
|
|
1167
|
+
elif ui.sel >= ui.scroll + (list_h // 5):
|
|
1168
|
+
ui.scroll = ui.sel - (list_h // 5) + 1
|
|
1169
|
+
|
|
1170
|
+
y = 4
|
|
1171
|
+
for i in range(len(ui.personas)):
|
|
1172
|
+
if i < ui.scroll:
|
|
1173
|
+
continue
|
|
1174
|
+
if y >= height - 3:
|
|
1175
|
+
break
|
|
1176
|
+
p = ui.personas[i]
|
|
1177
|
+
selected = (i == ui.sel)
|
|
1178
|
+
prefix = "\033[7m>" if selected else " "
|
|
1179
|
+
hyp_idx = p.get('hypothesis_idx', 0)
|
|
1180
|
+
hyp_text = ui.hypotheses[hyp_idx][:60] if hyp_idx < len(ui.hypotheses) else "?"
|
|
1181
|
+
|
|
1182
|
+
out.append(f"\033[{y};1H\033[K{prefix} \033[1m{p['name']}\033[0m \033[90m(b.{p['birth_year']}, {p.get('location', '?')})\033[0m")
|
|
1183
|
+
y += 1
|
|
1184
|
+
if selected and y < height - 3:
|
|
1185
|
+
out.append(f"\033[{y};5H\033[K\033[90mLeader: {p.get('leader', '?')}\033[0m")
|
|
1186
|
+
y += 1
|
|
1187
|
+
interests = p.get('interests', [])
|
|
1188
|
+
if isinstance(interests, list):
|
|
1189
|
+
interests = ', '.join(interests)
|
|
1190
|
+
if y < height - 3:
|
|
1191
|
+
out.append(f"\033[{y};5H\033[K\033[90mInterests: {str(interests)[:width-20]}\033[0m")
|
|
1192
|
+
y += 1
|
|
1193
|
+
if y < height - 3:
|
|
1194
|
+
out.append(f"\033[{y};5H\033[K\033[90mWorldview: {p.get('worldview', '')[:width-20]}\033[0m")
|
|
1195
|
+
y += 1
|
|
1196
|
+
if y < height - 3:
|
|
1197
|
+
out.append(f"\033[{y};5H\033[K\033[36mHypothesis: {hyp_text}\033[0m")
|
|
1198
|
+
y += 1
|
|
1199
|
+
y += 1
|
|
1200
|
+
|
|
1201
|
+
while y < height - 2:
|
|
1202
|
+
out.append(f"\033[{y};1H\033[K")
|
|
1203
|
+
y += 1
|
|
1204
|
+
|
|
1205
|
+
gen = " (generating...)" if ui.generating else ""
|
|
1206
|
+
out.append(f"\033[{height};1H\033[K\033[7m j/k:Nav e:Edit r:Regen s:Swap hyp Enter:Launch A:Auto q:Quit{gen} \033[0m".ljust(width))
|
|
1207
|
+
|
|
1208
|
+
def render_phase_execution(out, width, height):
|
|
1209
|
+
left_w = max(30, width // 3)
|
|
1210
|
+
right_w = width - left_w - 1
|
|
1211
|
+
panel_h = height - 4
|
|
1212
|
+
|
|
1213
|
+
out.append(f"\033[3;1H\033[36;1m Agent Info \033[90m{'─' * (left_w - 13)}\033[0m")
|
|
1214
|
+
|
|
1215
|
+
if 0 <= ui.current_agent < len(ui.personas):
|
|
1216
|
+
p = ui.personas[ui.current_agent]
|
|
1217
|
+
hi = p.get('hypothesis_idx', 0)
|
|
1218
|
+
hyp = ui.hypotheses[hi] if hi < len(ui.hypotheses) else "?"
|
|
1219
|
+
y = 4
|
|
1220
|
+
out.append(f"\033[{y};2H\033[1m{p['name']}\033[0m"); y += 1
|
|
1221
|
+
out.append(f"\033[{y};2H\033[90mb.{p['birth_year']}, {p.get('location', '?')[:left_w-10]}\033[0m"); y += 1
|
|
1222
|
+
out.append(f"\033[{y};2H\033[90mLeader: {p.get('leader', '?')[:left_w-12]}\033[0m"); y += 1
|
|
1223
|
+
interests = p.get('interests', [])
|
|
1224
|
+
if isinstance(interests, list):
|
|
1225
|
+
interests = ', '.join(interests)
|
|
1226
|
+
out.append(f"\033[{y};2H\033[90m{str(interests)[:left_w-4]}\033[0m"); y += 1
|
|
1227
|
+
y += 1
|
|
1228
|
+
out.append(f"\033[{y};2H\033[36mHypothesis:\033[0m"); y += 1
|
|
1229
|
+
for hl in wrap_text(str(hyp), left_w - 4)[:3]:
|
|
1230
|
+
out.append(f"\033[{y};3H{hl[:left_w-4]}"); y += 1
|
|
1231
|
+
y += 1
|
|
1232
|
+
done = sum(1 for t in ui.traces if t)
|
|
1233
|
+
out.append(f"\033[{y};2H\033[90mAgent {ui.current_agent+1}/{len(ui.hypotheses)}\033[0m"); y += 1
|
|
1234
|
+
out.append(f"\033[{y};2H\033[90mDone: {done}\033[0m")
|
|
1235
|
+
else:
|
|
1236
|
+
out.append(f"\033[4;2H\033[90mWaiting...\033[0m")
|
|
1237
|
+
|
|
1238
|
+
out.append(f"\033[3;{left_w+1}H\033[33;1m Live Output \033[90m{'─' * (right_w - 14)}\033[0m")
|
|
1239
|
+
|
|
1240
|
+
log_h = panel_h - 1
|
|
1241
|
+
total = len(ui.exec_log)
|
|
1242
|
+
visible_start = max(0, total - log_h - ui.exec_scroll)
|
|
1243
|
+
|
|
1244
|
+
for i in range(log_h):
|
|
1245
|
+
idx = visible_start + i
|
|
1246
|
+
row = 4 + i
|
|
1247
|
+
out.append(f"\033[{row};{left_w+1}H\033[K")
|
|
1248
|
+
if 0 <= idx < total:
|
|
1249
|
+
out.append(f"\033[{row};{left_w+2}H{ui.exec_log[idx][:right_w-2]}")
|
|
1250
|
+
|
|
1251
|
+
pause_text = " \033[31m[PAUSED]\033[0m" if ui.exec_paused else ""
|
|
1252
|
+
done_count = len(ui.traces)
|
|
1253
|
+
cycle_text = f"[Cycle {ui.current_cycle}/{ui.total_cycles}] " if ui.current_cycle > 0 else ""
|
|
1254
|
+
progress = f"{cycle_text}[Agent {ui.current_agent+1}/{len(ui.hypotheses)}] [Done: {done_count}]"
|
|
1255
|
+
out.append(f"\033[{height-1};1H\033[K\033[90m{progress}\033[0m")
|
|
1256
|
+
out.append(f"\033[{height};1H\033[K\033[7m s:Skip p:Pause x:End cycles j/k:Scroll q:Quit{pause_text} \033[0m".ljust(width))
|
|
1257
|
+
|
|
1258
|
+
def render_phase_review(out, width, height):
|
|
1259
|
+
tabs = ['Agents', 'Insights', 'Files', 'Paper']
|
|
1260
|
+
tab_bar = " "
|
|
1261
|
+
for i, tab in enumerate(tabs):
|
|
1262
|
+
if i == ui.review_tab:
|
|
1263
|
+
tab_bar += f'\033[43;30;1m {tab} \033[0m '
|
|
1264
|
+
else:
|
|
1265
|
+
tab_bar += f'\033[90m {tab} \033[0m '
|
|
1266
|
+
out.append(f"\033[3;1H{tab_bar}")
|
|
1267
|
+
out.append(f"\033[4;1H\033[90m{'─' * width}\033[0m")
|
|
1268
|
+
|
|
1269
|
+
content_h = height - 7
|
|
1270
|
+
content_lines = []
|
|
1271
|
+
|
|
1272
|
+
if ui.review_tab == 0:
|
|
1273
|
+
for i, trace in enumerate(ui.traces):
|
|
1274
|
+
sel = (i == ui.sel)
|
|
1275
|
+
prefix = "\033[7m>" if sel else " "
|
|
1276
|
+
status = "\033[32mSUCCESS\033[0m" if trace.was_successful else "\033[90mno files\033[0m"
|
|
1277
|
+
content_lines.append(f"{prefix} \033[1m{trace.agent_name}\033[0m [{status}] Steps: {len(trace.steps)} Files: {len(trace.final_files)}")
|
|
1278
|
+
if sel:
|
|
1279
|
+
content_lines.append(f" \033[36mHypothesis: {trace.hypothesis[:width-20]}\033[0m")
|
|
1280
|
+
content_lines.append(f" \033[90mPersona: {trace.agent_persona[:width-14]}\033[0m")
|
|
1281
|
+
if trace.final_files:
|
|
1282
|
+
content_lines.append(f" \033[90mFiles: {', '.join(trace.final_files.keys())}\033[0m")
|
|
1283
|
+
content_lines.append("")
|
|
1284
|
+
|
|
1285
|
+
elif ui.review_tab == 1:
|
|
1286
|
+
for i, insight in enumerate(ui.all_insights):
|
|
1287
|
+
sel = (i == ui.sel)
|
|
1288
|
+
starred = "★ " if i in ui.starred else " "
|
|
1289
|
+
prefix = "\033[7m>" if sel else " "
|
|
1290
|
+
star_color = "\033[33m" if i in ui.starred else ""
|
|
1291
|
+
content_lines.append(f"{prefix}{star_color}{starred}{insight[:width-8]}\033[0m")
|
|
1292
|
+
|
|
1293
|
+
elif ui.review_tab == 2:
|
|
1294
|
+
if not ui.all_files:
|
|
1295
|
+
content_lines.append(" No files created during research.")
|
|
1296
|
+
for i, finfo in enumerate(ui.all_files):
|
|
1297
|
+
sel = (i == ui.sel)
|
|
1298
|
+
prefix = "\033[7m>" if sel else " "
|
|
1299
|
+
content_lines.append(f"{prefix} \033[1m{finfo['name']}\033[0m \033[90m(by {finfo['agent']})\033[0m")
|
|
1300
|
+
if sel and finfo.get('content'):
|
|
1301
|
+
for pl in wrap_text(finfo['content'], width - 8)[:6]:
|
|
1302
|
+
content_lines.append(f" \033[90m{pl}\033[0m")
|
|
1303
|
+
content_lines.append("")
|
|
1304
|
+
|
|
1305
|
+
elif ui.review_tab == 3:
|
|
1306
|
+
if not ui.paper:
|
|
1307
|
+
content_lines.append(" No paper generated yet.")
|
|
1308
|
+
else:
|
|
1309
|
+
content_lines = wrap_text(ui.paper, width - 4)
|
|
1310
|
+
|
|
1311
|
+
if ui.scroll > max(0, len(content_lines) - 1):
|
|
1312
|
+
ui.scroll = max(0, len(content_lines) - 1)
|
|
1313
|
+
|
|
1314
|
+
for i in range(content_h):
|
|
1315
|
+
idx = ui.scroll + i
|
|
1316
|
+
row = 5 + i
|
|
1317
|
+
out.append(f"\033[{row};1H\033[K")
|
|
1318
|
+
if idx < len(content_lines):
|
|
1319
|
+
out.append(f"\033[{row};2H{content_lines[idx][:width-2]}")
|
|
1320
|
+
|
|
1321
|
+
if len(content_lines) > content_h and content_h > 0:
|
|
1322
|
+
pct = ui.scroll / max(1, len(content_lines) - content_h)
|
|
1323
|
+
pos = int(pct * (content_h - 1))
|
|
1324
|
+
out.append(f"\033[{5+pos};{width}H\033[33m▐\033[0m")
|
|
1325
|
+
|
|
1326
|
+
gen = " (generating...)" if ui.generating else ""
|
|
1327
|
+
out.append(f"\033[{height};1H\033[K\033[7m Tab/h/l:Tabs j/k:Nav s:Star Enter:View q:Quit{gen} \033[0m".ljust(width))
|
|
1328
|
+
|
|
1329
|
+
# ========== Input Handling ==========
|
|
1330
|
+
def handle_input(c, fd):
|
|
1331
|
+
if ui.mode == 'edit':
|
|
1332
|
+
return handle_edit_input(c, fd)
|
|
1333
|
+
|
|
1334
|
+
if c == '\x1b':
|
|
1335
|
+
if _sel.select([fd], [], [], 0.05)[0]:
|
|
1336
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
1337
|
+
if c2 == '[':
|
|
1338
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
1339
|
+
if c3 == 'A': nav_up()
|
|
1340
|
+
elif c3 == 'B': nav_down()
|
|
1341
|
+
elif c3 == 'C': nav_right()
|
|
1342
|
+
elif c3 == 'D': nav_left()
|
|
1343
|
+
else:
|
|
1344
|
+
return False
|
|
1345
|
+
return True
|
|
1346
|
+
|
|
1347
|
+
if c == 'q' or c == '\x03':
|
|
1348
|
+
return False
|
|
1349
|
+
|
|
1350
|
+
if c == 'A' and ui.phase < 3:
|
|
1351
|
+
ui.auto_mode = True
|
|
1352
|
+
ui.status = "Auto mode enabled"
|
|
1353
|
+
advance_phase()
|
|
1354
|
+
return True
|
|
1355
|
+
|
|
1356
|
+
if ui.phase == 0:
|
|
1357
|
+
return handle_phase0(c, fd)
|
|
1358
|
+
elif ui.phase == 1:
|
|
1359
|
+
return handle_phase1(c, fd)
|
|
1360
|
+
elif ui.phase == 2:
|
|
1361
|
+
return handle_phase2(c, fd)
|
|
1362
|
+
elif ui.phase == 3:
|
|
1363
|
+
return handle_phase3(c, fd)
|
|
1364
|
+
elif ui.phase == 4:
|
|
1365
|
+
return handle_phase4(c, fd)
|
|
1366
|
+
return True
|
|
1367
|
+
|
|
1368
|
+
def handle_edit_input(c, fd):
|
|
1369
|
+
if c == '\x1b':
|
|
1370
|
+
if _sel.select([fd], [], [], 0.05)[0]:
|
|
1371
|
+
os.read(fd, 2)
|
|
1372
|
+
ui.mode = 'normal'
|
|
1373
|
+
ui.input_buf = ""
|
|
1374
|
+
return True
|
|
1375
|
+
if c in ('\r', '\n'):
|
|
1376
|
+
save_edit()
|
|
1377
|
+
ui.mode = 'normal'
|
|
1378
|
+
return True
|
|
1379
|
+
if c == '\x7f' or c == '\x08':
|
|
1380
|
+
ui.input_buf = ui.input_buf[:-1]
|
|
1381
|
+
return True
|
|
1382
|
+
if c >= ' ' and c <= '~':
|
|
1383
|
+
ui.input_buf += c
|
|
1384
|
+
return True
|
|
1385
|
+
|
|
1386
|
+
def save_edit():
|
|
1387
|
+
buf = ui.input_buf.strip()
|
|
1388
|
+
if not buf:
|
|
1389
|
+
return
|
|
1390
|
+
target = ui.input_target
|
|
1391
|
+
if target.startswith("hyp:"):
|
|
1392
|
+
idx = int(target.split(":")[1])
|
|
1393
|
+
if idx < len(ui.hypotheses):
|
|
1394
|
+
ui.hypotheses[idx] = buf
|
|
1395
|
+
elif target == "new_hyp":
|
|
1396
|
+
ui.hypotheses.append(buf)
|
|
1397
|
+
ui.sel = len(ui.hypotheses) - 1
|
|
1398
|
+
elif target.startswith("persona_name:"):
|
|
1399
|
+
idx = int(target.split(":")[1])
|
|
1400
|
+
if idx < len(ui.personas):
|
|
1401
|
+
ui.personas[idx]['name'] = buf
|
|
1402
|
+
|
|
1403
|
+
def nav_up():
|
|
1404
|
+
if ui.sel > 0:
|
|
1405
|
+
ui.sel -= 1
|
|
1406
|
+
def nav_down():
|
|
1407
|
+
mx = get_max_sel()
|
|
1408
|
+
if ui.sel < mx:
|
|
1409
|
+
ui.sel += 1
|
|
1410
|
+
def nav_right():
|
|
1411
|
+
if ui.phase == 4:
|
|
1412
|
+
ui.review_tab = (ui.review_tab + 1) % 4
|
|
1413
|
+
ui.sel = 0; ui.scroll = 0
|
|
1414
|
+
def nav_left():
|
|
1415
|
+
if ui.phase == 4:
|
|
1416
|
+
ui.review_tab = (ui.review_tab - 1) % 4
|
|
1417
|
+
ui.sel = 0; ui.scroll = 0
|
|
1418
|
+
|
|
1419
|
+
def get_max_sel():
|
|
1420
|
+
if ui.phase == 1: return max(0, len(ui.hypotheses) - 1)
|
|
1421
|
+
elif ui.phase == 2: return max(0, len(ui.personas) - 1)
|
|
1422
|
+
elif ui.phase == 4:
|
|
1423
|
+
if ui.review_tab == 0: return max(0, len(ui.traces) - 1)
|
|
1424
|
+
elif ui.review_tab == 1: return max(0, len(ui.all_insights) - 1)
|
|
1425
|
+
elif ui.review_tab == 2: return max(0, len(ui.all_files) - 1)
|
|
1426
|
+
return 0
|
|
1427
|
+
|
|
1428
|
+
def handle_phase0(c, fd):
|
|
1429
|
+
if c in ('\r', '\n'):
|
|
1430
|
+
advance_phase()
|
|
1431
|
+
return True
|
|
1432
|
+
|
|
1433
|
+
def handle_phase1(c, fd):
|
|
1434
|
+
if c == 'j': nav_down()
|
|
1435
|
+
elif c == 'k': nav_up()
|
|
1436
|
+
elif c == 'e' and not ui.generating and ui.hypotheses:
|
|
1437
|
+
ui.mode = 'edit'
|
|
1438
|
+
ui.input_target = f"hyp:{ui.sel}"
|
|
1439
|
+
ui.input_buf = ui.hypotheses[ui.sel] if ui.sel < len(ui.hypotheses) else ""
|
|
1440
|
+
elif c == 'd' and not ui.generating and len(ui.hypotheses) > 1:
|
|
1441
|
+
del ui.hypotheses[ui.sel]
|
|
1442
|
+
ui.sel = clamp(ui.sel, 0, len(ui.hypotheses) - 1)
|
|
1443
|
+
elif c == '+' and not ui.generating:
|
|
1444
|
+
ui.mode = 'edit'
|
|
1445
|
+
ui.input_target = "new_hyp"
|
|
1446
|
+
ui.input_buf = ""
|
|
1447
|
+
elif c == 'r' and not ui.generating:
|
|
1448
|
+
threading.Thread(target=do_generate_hypotheses, daemon=True).start()
|
|
1449
|
+
elif c in ('\r', '\n') and not ui.generating:
|
|
1450
|
+
advance_phase()
|
|
1451
|
+
return True
|
|
1452
|
+
|
|
1453
|
+
def handle_phase2(c, fd):
|
|
1454
|
+
if c == 'j': nav_down()
|
|
1455
|
+
elif c == 'k': nav_up()
|
|
1456
|
+
elif c == 'e' and not ui.generating and ui.personas:
|
|
1457
|
+
ui.mode = 'edit'
|
|
1458
|
+
ui.input_target = f"persona_name:{ui.sel}"
|
|
1459
|
+
ui.input_buf = ui.personas[ui.sel]['name'] if ui.sel < len(ui.personas) else ""
|
|
1460
|
+
elif c == 'r' and not ui.generating:
|
|
1461
|
+
threading.Thread(target=do_generate_personas, daemon=True).start()
|
|
1462
|
+
elif c == 's' and not ui.generating and ui.personas:
|
|
1463
|
+
p = ui.personas[ui.sel]
|
|
1464
|
+
p['hypothesis_idx'] = (p['hypothesis_idx'] + 1) % len(ui.hypotheses)
|
|
1465
|
+
elif c in ('\r', '\n') and not ui.generating:
|
|
1466
|
+
advance_phase()
|
|
1467
|
+
return True
|
|
1468
|
+
|
|
1469
|
+
def handle_phase3(c, fd):
|
|
1470
|
+
if c == 's':
|
|
1471
|
+
ui.exec_skip = True
|
|
1472
|
+
ui.agent_ui['skip'] = True
|
|
1473
|
+
elif c == 'p':
|
|
1474
|
+
ui.exec_paused = not ui.exec_paused
|
|
1475
|
+
ui.agent_ui['paused'] = ui.exec_paused
|
|
1476
|
+
ui.status = "Paused" if ui.exec_paused else "Resumed"
|
|
1477
|
+
elif c == 'x':
|
|
1478
|
+
ui.end_cycles = True
|
|
1479
|
+
ui.status = "Ending review cycles after current operation..."
|
|
1480
|
+
elif c == 'j':
|
|
1481
|
+
ui.exec_scroll = max(0, ui.exec_scroll - 1)
|
|
1482
|
+
elif c == 'k':
|
|
1483
|
+
ui.exec_scroll += 1
|
|
1484
|
+
return True
|
|
1485
|
+
|
|
1486
|
+
def handle_phase4(c, fd):
|
|
1487
|
+
if c == 'j': nav_down()
|
|
1488
|
+
elif c == 'k': nav_up()
|
|
1489
|
+
elif c == '\t' or c == 'l': nav_right()
|
|
1490
|
+
elif c == 'h': nav_left()
|
|
1491
|
+
elif c == 's' and ui.review_tab == 1 and ui.sel < len(ui.all_insights):
|
|
1492
|
+
if ui.sel in ui.starred: ui.starred.discard(ui.sel)
|
|
1493
|
+
else: ui.starred.add(ui.sel)
|
|
1494
|
+
elif c in ('\r', '\n'):
|
|
1495
|
+
show_full_item(fd)
|
|
1496
|
+
return True
|
|
1497
|
+
|
|
1498
|
+
def show_full_item(fd):
|
|
1499
|
+
content = ""
|
|
1500
|
+
if ui.review_tab == 0 and ui.sel < len(ui.traces):
|
|
1501
|
+
t = ui.traces[ui.sel]
|
|
1502
|
+
content = f"Agent: {t.agent_name}\nHypothesis: {t.hypothesis}\nSuccess: {t.was_successful}\nPersona: {t.agent_persona}\n\n"
|
|
1503
|
+
for s in t.steps:
|
|
1504
|
+
content += f"--- Step {s.step} ---\nThought: {s.thought[:500]}\nAction: {s.action[:300]}\nOutcome: {s.outcome[:300]}\n\n"
|
|
1505
|
+
if t.final_files:
|
|
1506
|
+
content += f"Files: {', '.join(t.final_files.keys())}\n"
|
|
1507
|
+
elif ui.review_tab == 1 and ui.sel < len(ui.all_insights):
|
|
1508
|
+
content = ui.all_insights[ui.sel]
|
|
1509
|
+
elif ui.review_tab == 2 and ui.sel < len(ui.all_files):
|
|
1510
|
+
f = ui.all_files[ui.sel]
|
|
1511
|
+
content = f"File: {f['name']}\nAgent: {f['agent']}\n\n{f.get('content', 'N/A')}"
|
|
1512
|
+
elif ui.review_tab == 3:
|
|
1513
|
+
content = ui.paper
|
|
1514
|
+
|
|
1515
|
+
if not content:
|
|
1516
|
+
return
|
|
1517
|
+
|
|
1518
|
+
width, height = get_size()
|
|
1519
|
+
lines = wrap_text(content, width - 4)
|
|
1520
|
+
scroll = 0
|
|
1521
|
+
|
|
1522
|
+
sys.stdout.write('\033[2J\033[H')
|
|
1523
|
+
while True:
|
|
1524
|
+
sys.stdout.write('\033[H')
|
|
1525
|
+
for i in range(height - 2):
|
|
1526
|
+
idx = scroll + i
|
|
1527
|
+
sys.stdout.write(f'\033[{i+1};1H\033[K')
|
|
1528
|
+
if idx < len(lines):
|
|
1529
|
+
sys.stdout.write(f' {lines[idx][:width-4]}')
|
|
1530
|
+
sys.stdout.write(f'\033[{height};1H\033[K\033[7m j/k:Scroll q/Enter:Back [{scroll+1}-{min(scroll+height-2, len(lines))}/{len(lines)}] \033[0m')
|
|
1531
|
+
sys.stdout.flush()
|
|
1532
|
+
|
|
1533
|
+
c = os.read(fd, 1).decode('latin-1')
|
|
1534
|
+
if c in ('q', '\r', '\n'):
|
|
1535
|
+
break
|
|
1536
|
+
if c == '\x1b':
|
|
1537
|
+
if _sel.select([fd], [], [], 0.05)[0]:
|
|
1538
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
1539
|
+
if c2 == '[':
|
|
1540
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
1541
|
+
if c3 == 'A': scroll = max(0, scroll - 1)
|
|
1542
|
+
elif c3 == 'B': scroll = min(max(0, len(lines) - (height - 2)), scroll + 1)
|
|
1543
|
+
else:
|
|
1544
|
+
break
|
|
1545
|
+
elif c == 'k': scroll = max(0, scroll - 1)
|
|
1546
|
+
elif c == 'j': scroll = min(max(0, len(lines) - (height - 2)), scroll + 1)
|
|
1547
|
+
|
|
1548
|
+
def advance_phase():
|
|
1549
|
+
if ui.phase == 0:
|
|
1550
|
+
ui.phase = 1; ui.sel = 0; ui.scroll = 0; ui.error = ""
|
|
1551
|
+
threading.Thread(target=do_generate_hypotheses, daemon=True).start()
|
|
1552
|
+
if ui.auto_mode:
|
|
1553
|
+
threading.Thread(target=_auto_wait_then_advance, args=(1,), daemon=True).start()
|
|
1554
|
+
elif ui.phase == 1:
|
|
1555
|
+
ui.phase = 2; ui.sel = 0; ui.scroll = 0; ui.error = ""
|
|
1556
|
+
threading.Thread(target=do_generate_personas, daemon=True).start()
|
|
1557
|
+
if ui.auto_mode:
|
|
1558
|
+
threading.Thread(target=_auto_wait_then_advance, args=(2,), daemon=True).start()
|
|
1559
|
+
elif ui.phase == 2:
|
|
1560
|
+
ui.phase = 3; ui.sel = 0; ui.scroll = 0; ui.error = ""
|
|
1561
|
+
threading.Thread(target=do_run_all_agents, daemon=True).start()
|
|
1562
|
+
|
|
1563
|
+
def _auto_wait_then_advance(expected_phase):
|
|
1564
|
+
while ui.generating:
|
|
1565
|
+
time.sleep(0.3)
|
|
1566
|
+
if ui.phase == expected_phase and ui.auto_mode:
|
|
1567
|
+
time.sleep(0.5)
|
|
1568
|
+
advance_phase()
|
|
1569
|
+
|
|
1570
|
+
# ========== Main TUI Loop ==========
|
|
1571
|
+
if not sys.stdin.isatty():
|
|
1572
|
+
os.chdir(_orig_cwd)
|
|
1573
|
+
context['output'] = "Alicanto requires an interactive terminal."
|
|
1574
|
+
context['messages'] = messages
|
|
1575
|
+
exit()
|
|
1576
|
+
|
|
1577
|
+
ui.phase = 0
|
|
1578
|
+
ui.status = "Press Enter to begin, A for auto mode"
|
|
1579
|
+
|
|
1580
|
+
fd = sys.stdin.fileno()
|
|
1581
|
+
old_settings = termios.tcgetattr(fd)
|
|
1582
|
+
|
|
1583
|
+
try:
|
|
1584
|
+
tty.setcbreak(fd)
|
|
1585
|
+
sys.stdout.write('\033[?25l')
|
|
1586
|
+
sys.stdout.flush()
|
|
1587
|
+
render_screen()
|
|
1588
|
+
|
|
1589
|
+
running = True
|
|
1590
|
+
while running:
|
|
1591
|
+
if ui.generating:
|
|
1592
|
+
if _sel.select([fd], [], [], 0.3)[0]:
|
|
1593
|
+
c = os.read(fd, 1).decode('latin-1')
|
|
1594
|
+
running = handle_input(c, fd)
|
|
1595
|
+
else:
|
|
1596
|
+
if _sel.select([fd], [], [], 0.5)[0]:
|
|
1597
|
+
c = os.read(fd, 1).decode('latin-1')
|
|
1598
|
+
running = handle_input(c, fd)
|
|
1599
|
+
# Sync agent_ui state
|
|
1600
|
+
ui.agent_ui['skip'] = ui.exec_skip
|
|
1601
|
+
ui.agent_ui['paused'] = ui.exec_paused
|
|
1602
|
+
render_screen()
|
|
1603
|
+
|
|
1604
|
+
finally:
|
|
1605
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
1606
|
+
sys.stdout.write('\033[?25h')
|
|
1607
|
+
sys.stdout.write('\033[2J\033[H')
|
|
1608
|
+
sys.stdout.flush()
|
|
1609
|
+
os.chdir(_orig_cwd)
|
|
1610
|
+
|
|
1611
|
+
if ui.traces:
|
|
1612
|
+
print("\033[1;36m=== ALICANTO RESEARCH COMPLETE ===\033[0m\n")
|
|
1613
|
+
print(f"Query: {ui.query}")
|
|
1614
|
+
print(f"Agents: {len(ui.traces)}")
|
|
1615
|
+
for i, trace in enumerate(ui.traces):
|
|
1616
|
+
status = "SUCCESS" if trace.was_successful else "no files"
|
|
1617
|
+
print(f"\n{i+1}. \033[1m{trace.agent_name}\033[0m [{status}]")
|
|
1618
|
+
print(f" Hypothesis: {trace.hypothesis[:100]}")
|
|
1619
|
+
print(f" Steps: {len(trace.steps)} Files: {list(trace.final_files.keys())}")
|
|
1620
|
+
if ui.paper:
|
|
1621
|
+
print(f"\n\033[1;32m--- Paper ---\033[0m")
|
|
1622
|
+
print(ui.paper[:2000])
|
|
1623
|
+
print(f"\n\033[90mOutput directory: {_work_dir}\033[0m")
|
|
1624
|
+
|
|
1625
|
+
context['output'] = ui.paper or "Alicanto session ended."
|
|
1626
|
+
context['messages'] = messages
|
|
1627
|
+
context['alicanto_result'] = {
|
|
1628
|
+
'query': ui.query,
|
|
1629
|
+
'hypotheses': ui.hypotheses,
|
|
1630
|
+
'personas': [{'name': p.get('name'), 'birth_year': p.get('birth_year'), 'persona_text': p.get('persona_text')} for p in ui.personas],
|
|
1631
|
+
'traces': [{'agent': t.agent_name, 'hypothesis': t.hypothesis, 'success': t.was_successful, 'steps': len(t.steps), 'files': list(t.final_files.keys())} for t in ui.traces],
|
|
1632
|
+
'paper': ui.paper,
|
|
1633
|
+
}
|