@researai/deepscientist 1.5.1 → 1.5.3
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.
- package/README.md +69 -1
- package/bin/ds.js +2239 -153
- package/docs/en/00_QUICK_START.md +60 -20
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -20
- package/docs/en/02_START_RESEARCH_GUIDE.md +11 -11
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/en/05_TUI_GUIDE.md +1 -1
- package/docs/en/09_DOCTOR.md +48 -4
- package/docs/en/90_ARCHITECTURE.md +4 -2
- package/docs/zh/00_QUICK_START.md +60 -20
- package/docs/zh/01_SETTINGS_REFERENCE.md +21 -21
- package/docs/zh/02_START_RESEARCH_GUIDE.md +19 -19
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/zh/05_TUI_GUIDE.md +1 -1
- package/docs/zh/09_DOCTOR.md +46 -4
- package/install.sh +125 -8
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/service.py +553 -26
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +10 -19
- package/src/deepscientist/channels/discord_gateway.py +25 -2
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +22 -3
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +29 -5
- package/src/deepscientist/channels/telegram_polling.py +25 -2
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +27 -0
- package/src/deepscientist/config/models.py +6 -40
- package/src/deepscientist/config/service.py +165 -156
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +65 -11
- package/src/deepscientist/daemon/api/router.py +4 -2
- package/src/deepscientist/daemon/app.py +772 -219
- package/src/deepscientist/doctor.py +69 -2
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +25 -2
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +66 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/prompts/builder.py +71 -3
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +70 -12
- package/src/deepscientist/quest/stage_views.py +46 -0
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/prompts/connectors/lingzhu.md +3 -0
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +123 -10
- package/src/skills/analysis-campaign/SKILL.md +35 -6
- package/src/skills/baseline/SKILL.md +73 -32
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +28 -6
- package/src/skills/finalize/SKILL.md +5 -2
- package/src/skills/idea/SKILL.md +2 -2
- package/src/skills/intake-audit/SKILL.md +2 -2
- package/src/skills/rebuttal/SKILL.md +4 -2
- package/src/skills/review/SKILL.md +4 -2
- package/src/skills/scout/SKILL.md +2 -2
- package/src/skills/write/SKILL.md +2 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-w5lF2Ttt.js → AiManusChatView-qzChi9uh.js} +67 -94
- package/src/ui/dist/assets/{AnalysisPlugin-DJOED79I.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-DaG61Y0M.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CV4LqUB_.js → CliPlugin-DJJFfVmW.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DylfAea4.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-F7saY0LM.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-COP0c7jf.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CAS05pT9.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-Bco1CN_w.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CvMlCD99.js → LabCopilotPanel-dfLptQcR.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BYankkE4.js → LabPlugin-CeGjAl3A.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-LDSMR-t-.js → LatexPlugin-BBJ7kd1V.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-B7o80jgm.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CM6ZOcpC.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-Dc61cXmK.js → NotebookEditor-4R88_BMO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DWowuQwx.js → PdfLoader-DwEFQLrw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BsJM1q_a.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DB2eEEFQ.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CraThSvt.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
- package/src/ui/dist/assets/{Stepper-CgocRTPq.js → Stepper-ClOgzWM3.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-B1JGhKtd.js → TextViewerPlugin-DDQWxibk.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-CclFC7FM.js → VNCViewer-CJXT0Nm8.js} +9 -9
- package/src/ui/dist/assets/{bibtex-D3IKsMl7.js → bibtex-DLr4Rtk4.js} +1 -1
- package/src/ui/dist/assets/{code-BP37Xx0p.js → code-DgKK408Y.js} +1 -1
- package/src/ui/dist/assets/{file-content-BAJSu-9r.js → file-content-6HBqQnvQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DUGeCTuy.js → file-diff-panel-Dhu0TbBM.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CXc1Ojf7.js → file-socket-CP3iwVZG.js} +1 -1
- package/src/ui/dist/assets/{file-utils-2J21jt7M.js → file-utils-BsS-Aw68.js} +1 -1
- package/src/ui/dist/assets/{image-CMMmgvcn.js → image-ByeK-Zcv.js} +1 -1
- package/src/ui/dist/assets/{index-DmwmJmbW.js → index-BLjo5--a.js} +33610 -31016
- package/src/ui/dist/assets/{index-CWgMgpow.js → index-BdsE0uRz.js} +11 -11
- package/src/ui/dist/assets/{index-s7aHnNQ4.js → index-C-eX-N6A.js} +1 -1
- package/src/ui/dist/assets/{index-KGt-z-dD.css → index-CuQhlrR-.css} +2747 -2
- package/src/ui/dist/assets/{index-BaVumsQT.js → index-DyremSIv.js} +2 -2
- package/src/ui/dist/assets/{message-square-CQRfX0Am.js → message-square-DnagiLnc.js} +1 -1
- package/src/ui/dist/assets/{monaco-B4TbdsrF.js → monaco-4kBFeprs.js} +1 -1
- package/src/ui/dist/assets/{popover-B8Rokodk.js → popover-hRCXZzs2.js} +1 -1
- package/src/ui/dist/assets/{project-sync-D_i96KH4.js → project-sync-O_85YuP6.js} +1 -1
- package/src/ui/dist/assets/{sigma-D12PnzCN.js → sigma-DvKopSnL.js} +1 -1
- package/src/ui/dist/assets/{tooltip-B6YrI4aJ.js → tooltip-BmlPc6kc.js} +1 -1
- package/src/ui/dist/assets/{trash-Bc8jGp0V.js → trash-n-UvdZFR.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-mXVCYSZ-.js → useCliAccess-WDd3_wIh.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Bg6b9H9K.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Drh5GEnL.js → wrap-text-qIYQ4a_W.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-CJj9DZLn.js → zoom-out-fZXCEFsy.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +0 -2698
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .shared import slugify
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
QQ_PROFILE_ID_PREFIX = "qq-profile"
|
|
10
|
+
QQ_DEFAULT_SECRET_ENV = "QQ_APP_SECRET"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_qq_profile() -> dict[str, Any]:
|
|
14
|
+
return {
|
|
15
|
+
"profile_id": None,
|
|
16
|
+
"enabled": True,
|
|
17
|
+
"app_id": None,
|
|
18
|
+
"app_secret": None,
|
|
19
|
+
"app_secret_env": QQ_DEFAULT_SECRET_ENV,
|
|
20
|
+
"bot_name": "DeepScientist",
|
|
21
|
+
"main_chat_id": None,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _as_text(value: Any) -> str | None:
|
|
26
|
+
text = str(value or "").strip()
|
|
27
|
+
return text or None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _profile_id_seed(*, profile_id: Any, app_id: Any, bot_name: Any, index: int) -> str:
|
|
31
|
+
explicit = _as_text(profile_id)
|
|
32
|
+
if explicit:
|
|
33
|
+
return explicit
|
|
34
|
+
app_text = _as_text(app_id)
|
|
35
|
+
if app_text:
|
|
36
|
+
return f"qq-{app_text}"
|
|
37
|
+
bot_text = slugify(str(bot_name or "").strip(), default="")
|
|
38
|
+
if bot_text:
|
|
39
|
+
return f"{QQ_PROFILE_ID_PREFIX}-{bot_text}"
|
|
40
|
+
return f"{QQ_PROFILE_ID_PREFIX}-{index:03d}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _unique_profile_id(seed: str, *, used: set[str]) -> str:
|
|
44
|
+
base = slugify(seed, default=QQ_PROFILE_ID_PREFIX)
|
|
45
|
+
candidate = base
|
|
46
|
+
suffix = 2
|
|
47
|
+
while candidate in used:
|
|
48
|
+
candidate = f"{base}-{suffix}"
|
|
49
|
+
suffix += 1
|
|
50
|
+
used.add(candidate)
|
|
51
|
+
return candidate
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def list_qq_profiles(config: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
55
|
+
normalized = normalize_qq_connector_config(config)
|
|
56
|
+
profiles = normalized.get("profiles")
|
|
57
|
+
return [dict(item) for item in profiles] if isinstance(profiles, list) else []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_qq_profile(
|
|
61
|
+
config: dict[str, Any] | None,
|
|
62
|
+
*,
|
|
63
|
+
profile_id: str | None = None,
|
|
64
|
+
app_id: str | None = None,
|
|
65
|
+
) -> dict[str, Any] | None:
|
|
66
|
+
normalized_profile_id = _as_text(profile_id)
|
|
67
|
+
normalized_app_id = _as_text(app_id)
|
|
68
|
+
for profile in list_qq_profiles(config):
|
|
69
|
+
if normalized_profile_id and str(profile.get("profile_id") or "").strip() == normalized_profile_id:
|
|
70
|
+
return profile
|
|
71
|
+
if normalized_app_id and str(profile.get("app_id") or "").strip() == normalized_app_id:
|
|
72
|
+
return profile
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def merge_qq_profile_config(shared_config: dict[str, Any] | None, profile: dict[str, Any]) -> dict[str, Any]:
|
|
77
|
+
normalized = normalize_qq_connector_config(shared_config)
|
|
78
|
+
merged = deepcopy(normalized)
|
|
79
|
+
merged.pop("profiles", None)
|
|
80
|
+
merged.update(
|
|
81
|
+
{
|
|
82
|
+
"profile_id": str(profile.get("profile_id") or "").strip() or None,
|
|
83
|
+
"app_id": _as_text(profile.get("app_id")),
|
|
84
|
+
"app_secret": _as_text(profile.get("app_secret")),
|
|
85
|
+
"app_secret_env": _as_text(profile.get("app_secret_env")) or QQ_DEFAULT_SECRET_ENV,
|
|
86
|
+
"bot_name": _as_text(profile.get("bot_name")) or str(normalized.get("bot_name") or "DeepScientist"),
|
|
87
|
+
"main_chat_id": _as_text(profile.get("main_chat_id")),
|
|
88
|
+
"enabled": bool(normalized.get("enabled", False)) and bool(profile.get("enabled", True)),
|
|
89
|
+
"transport": "gateway_direct",
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
return merged
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def qq_profile_label(profile: dict[str, Any] | None) -> str:
|
|
96
|
+
if not isinstance(profile, dict):
|
|
97
|
+
return "QQ"
|
|
98
|
+
bot_name = _as_text(profile.get("bot_name"))
|
|
99
|
+
app_id = _as_text(profile.get("app_id"))
|
|
100
|
+
if bot_name and app_id:
|
|
101
|
+
return f"{bot_name} · {app_id}"
|
|
102
|
+
if bot_name:
|
|
103
|
+
return bot_name
|
|
104
|
+
if app_id:
|
|
105
|
+
return f"QQ · {app_id}"
|
|
106
|
+
return "QQ"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def normalize_qq_connector_config(config: dict[str, Any] | None) -> dict[str, Any]:
|
|
110
|
+
payload = deepcopy(config or {})
|
|
111
|
+
shared_defaults = {
|
|
112
|
+
"enabled": False,
|
|
113
|
+
"transport": "gateway_direct",
|
|
114
|
+
"app_id": None,
|
|
115
|
+
"app_secret": None,
|
|
116
|
+
"app_secret_env": QQ_DEFAULT_SECRET_ENV,
|
|
117
|
+
"bot_name": "DeepScientist",
|
|
118
|
+
"command_prefix": "/",
|
|
119
|
+
"main_chat_id": None,
|
|
120
|
+
"require_at_in_groups": True,
|
|
121
|
+
"auto_bind_dm_to_active_quest": True,
|
|
122
|
+
"gateway_restart_on_config_change": True,
|
|
123
|
+
"auto_send_main_experiment_png": True,
|
|
124
|
+
"auto_send_analysis_summary_png": True,
|
|
125
|
+
"auto_send_slice_png": True,
|
|
126
|
+
"auto_send_paper_pdf": True,
|
|
127
|
+
"enable_markdown_send": False,
|
|
128
|
+
"enable_file_upload_experimental": False,
|
|
129
|
+
"profiles": [],
|
|
130
|
+
}
|
|
131
|
+
shared = {**shared_defaults, **payload}
|
|
132
|
+
shared["transport"] = "gateway_direct"
|
|
133
|
+
shared["command_prefix"] = _as_text(shared.get("command_prefix")) or "/"
|
|
134
|
+
shared["bot_name"] = _as_text(shared.get("bot_name")) or "DeepScientist"
|
|
135
|
+
shared["app_secret_env"] = _as_text(shared.get("app_secret_env")) or QQ_DEFAULT_SECRET_ENV
|
|
136
|
+
|
|
137
|
+
raw_profiles = payload.get("profiles")
|
|
138
|
+
items = list(raw_profiles) if isinstance(raw_profiles, list) else []
|
|
139
|
+
legacy_profile_seed = {
|
|
140
|
+
"app_id": payload.get("app_id"),
|
|
141
|
+
"app_secret": payload.get("app_secret"),
|
|
142
|
+
"app_secret_env": payload.get("app_secret_env"),
|
|
143
|
+
"bot_name": payload.get("bot_name"),
|
|
144
|
+
"main_chat_id": payload.get("main_chat_id"),
|
|
145
|
+
}
|
|
146
|
+
if not items:
|
|
147
|
+
if any(_as_text(legacy_profile_seed.get(key)) for key in ("app_id", "app_secret", "main_chat_id", "bot_name")):
|
|
148
|
+
items = [legacy_profile_seed]
|
|
149
|
+
|
|
150
|
+
profiles: list[dict[str, Any]] = []
|
|
151
|
+
used_ids: set[str] = set()
|
|
152
|
+
for index, raw in enumerate(items, start=1):
|
|
153
|
+
if not isinstance(raw, dict):
|
|
154
|
+
continue
|
|
155
|
+
current = {**default_qq_profile(), **raw}
|
|
156
|
+
current["enabled"] = bool(current.get("enabled", True))
|
|
157
|
+
current["app_id"] = _as_text(current.get("app_id"))
|
|
158
|
+
current["app_secret"] = _as_text(current.get("app_secret"))
|
|
159
|
+
current["app_secret_env"] = _as_text(current.get("app_secret_env")) or shared["app_secret_env"]
|
|
160
|
+
current["bot_name"] = _as_text(current.get("bot_name")) or shared["bot_name"]
|
|
161
|
+
current["main_chat_id"] = _as_text(current.get("main_chat_id"))
|
|
162
|
+
current["profile_id"] = _unique_profile_id(
|
|
163
|
+
_profile_id_seed(
|
|
164
|
+
profile_id=current.get("profile_id"),
|
|
165
|
+
app_id=current.get("app_id"),
|
|
166
|
+
bot_name=current.get("bot_name"),
|
|
167
|
+
index=index,
|
|
168
|
+
),
|
|
169
|
+
used=used_ids,
|
|
170
|
+
)
|
|
171
|
+
profiles.append(current)
|
|
172
|
+
|
|
173
|
+
shared["profiles"] = profiles
|
|
174
|
+
if len(profiles) == 1:
|
|
175
|
+
mirror = profiles[0]
|
|
176
|
+
shared["app_id"] = mirror.get("app_id")
|
|
177
|
+
shared["app_secret"] = mirror.get("app_secret")
|
|
178
|
+
shared["app_secret_env"] = mirror.get("app_secret_env")
|
|
179
|
+
shared["bot_name"] = mirror.get("bot_name")
|
|
180
|
+
shared["main_chat_id"] = mirror.get("main_chat_id")
|
|
181
|
+
else:
|
|
182
|
+
shared["app_id"] = None
|
|
183
|
+
shared["app_secret"] = None
|
|
184
|
+
shared["main_chat_id"] = None
|
|
185
|
+
|
|
186
|
+
return shared
|
|
@@ -1548,11 +1548,16 @@ class QuestService:
|
|
|
1548
1548
|
if normalized in seen_files:
|
|
1549
1549
|
return
|
|
1550
1550
|
seen_files.add(normalized)
|
|
1551
|
+
resolved_document_id = document_id or self._path_to_document_id(
|
|
1552
|
+
normalized,
|
|
1553
|
+
quest_root=quest_root,
|
|
1554
|
+
workspace_root=workspace_root,
|
|
1555
|
+
)
|
|
1551
1556
|
changed_files.append(
|
|
1552
1557
|
{
|
|
1553
1558
|
"path": normalized,
|
|
1554
1559
|
"source": source,
|
|
1555
|
-
"document_id":
|
|
1560
|
+
"document_id": resolved_document_id,
|
|
1556
1561
|
"writable": writable,
|
|
1557
1562
|
}
|
|
1558
1563
|
)
|
|
@@ -1621,14 +1626,30 @@ class QuestService:
|
|
|
1621
1626
|
"changed_files": changed_files[-30:],
|
|
1622
1627
|
}
|
|
1623
1628
|
|
|
1624
|
-
def events(
|
|
1629
|
+
def events(
|
|
1630
|
+
self,
|
|
1631
|
+
quest_id: str,
|
|
1632
|
+
*,
|
|
1633
|
+
after: int = 0,
|
|
1634
|
+
before: int | None = None,
|
|
1635
|
+
limit: int = 200,
|
|
1636
|
+
tail: bool = False,
|
|
1637
|
+
) -> dict:
|
|
1625
1638
|
records = self._read_cached_jsonl(self._quest_root(quest_id) / ".ds" / "events.jsonl")
|
|
1626
1639
|
normalized_limit = max(limit, 0)
|
|
1627
|
-
|
|
1640
|
+
direction = "after"
|
|
1641
|
+
if before is not None:
|
|
1642
|
+
direction = "before"
|
|
1643
|
+
end = max(int(before) - 1, 0)
|
|
1644
|
+
start = max(end - normalized_limit, 0)
|
|
1645
|
+
sliced = records[start:end]
|
|
1646
|
+
elif tail and normalized_limit > 0:
|
|
1647
|
+
direction = "tail"
|
|
1628
1648
|
start = max(len(records) - normalized_limit, 0)
|
|
1649
|
+
sliced = records[start : start + normalized_limit]
|
|
1629
1650
|
else:
|
|
1630
1651
|
start = max(after, 0)
|
|
1631
|
-
|
|
1652
|
+
sliced = records[start : start + normalized_limit]
|
|
1632
1653
|
enriched = []
|
|
1633
1654
|
for index, item in enumerate(sliced, start=start + 1):
|
|
1634
1655
|
enriched.append(
|
|
@@ -1638,11 +1659,23 @@ class QuestService:
|
|
|
1638
1659
|
**item,
|
|
1639
1660
|
}
|
|
1640
1661
|
)
|
|
1641
|
-
|
|
1662
|
+
if before is not None:
|
|
1663
|
+
next_cursor = start + len(sliced)
|
|
1664
|
+
else:
|
|
1665
|
+
next_cursor = len(records) if tail else start + len(sliced)
|
|
1666
|
+
oldest_cursor = enriched[0]["cursor"] if enriched else None
|
|
1667
|
+
newest_cursor = enriched[-1]["cursor"] if enriched else None
|
|
1668
|
+
if before is not None:
|
|
1669
|
+
has_more = start > 0
|
|
1670
|
+
else:
|
|
1671
|
+
has_more = start > 0 if tail else next_cursor < len(records)
|
|
1642
1672
|
return {
|
|
1643
1673
|
"quest_id": quest_id,
|
|
1644
1674
|
"cursor": next_cursor,
|
|
1645
|
-
"has_more":
|
|
1675
|
+
"has_more": has_more,
|
|
1676
|
+
"oldest_cursor": oldest_cursor,
|
|
1677
|
+
"newest_cursor": newest_cursor,
|
|
1678
|
+
"direction": direction,
|
|
1646
1679
|
"events": enriched,
|
|
1647
1680
|
}
|
|
1648
1681
|
|
|
@@ -1790,18 +1823,13 @@ class QuestService:
|
|
|
1790
1823
|
|
|
1791
1824
|
quest_root = self._quest_root(quest_id)
|
|
1792
1825
|
workspace_root = self.active_workspace_root(quest_root)
|
|
1793
|
-
changed_paths = {
|
|
1794
|
-
item["path"]: item
|
|
1795
|
-
for item in self.workflow(quest_id).get("changed_files", [])
|
|
1796
|
-
if item.get("path")
|
|
1797
|
-
}
|
|
1798
1826
|
git_status = self._git_status_map(workspace_root)
|
|
1799
1827
|
|
|
1800
1828
|
root_nodes = self._tree_children(
|
|
1801
1829
|
workspace_root,
|
|
1802
1830
|
workspace_root,
|
|
1803
1831
|
git_status=git_status,
|
|
1804
|
-
changed_paths=
|
|
1832
|
+
changed_paths={},
|
|
1805
1833
|
profile=profile,
|
|
1806
1834
|
)
|
|
1807
1835
|
sections = self._group_explorer_sections(root_nodes)
|
|
@@ -2036,6 +2064,36 @@ class QuestService:
|
|
|
2036
2064
|
return None, None
|
|
2037
2065
|
return document_id, None
|
|
2038
2066
|
|
|
2067
|
+
@staticmethod
|
|
2068
|
+
def _path_to_document_id(
|
|
2069
|
+
path: str | Path | None,
|
|
2070
|
+
*,
|
|
2071
|
+
quest_root: Path,
|
|
2072
|
+
workspace_root: Path,
|
|
2073
|
+
) -> str | None:
|
|
2074
|
+
if not path:
|
|
2075
|
+
return None
|
|
2076
|
+
try:
|
|
2077
|
+
candidate = Path(path).expanduser()
|
|
2078
|
+
if not candidate.is_absolute():
|
|
2079
|
+
candidate = (workspace_root / candidate).resolve()
|
|
2080
|
+
else:
|
|
2081
|
+
candidate = candidate.resolve()
|
|
2082
|
+
except OSError:
|
|
2083
|
+
return None
|
|
2084
|
+
|
|
2085
|
+
try:
|
|
2086
|
+
relative_to_workspace = candidate.relative_to(workspace_root.resolve()).as_posix()
|
|
2087
|
+
return f"path::{relative_to_workspace}"
|
|
2088
|
+
except ValueError:
|
|
2089
|
+
pass
|
|
2090
|
+
|
|
2091
|
+
try:
|
|
2092
|
+
relative_to_quest = candidate.relative_to(quest_root.resolve()).as_posix()
|
|
2093
|
+
return f"questpath::{relative_to_quest}"
|
|
2094
|
+
except ValueError:
|
|
2095
|
+
return None
|
|
2096
|
+
|
|
2039
2097
|
@staticmethod
|
|
2040
2098
|
def _markdown_asset_directory(relative_path: str) -> PurePosixPath:
|
|
2041
2099
|
base_path = PurePosixPath(relative_path)
|
|
@@ -62,6 +62,38 @@ def _field(label: str, value: object, *, tone: str = "default") -> dict[str, Any
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def _evaluation_summary(value: object) -> dict[str, Any]:
|
|
66
|
+
if not isinstance(value, dict):
|
|
67
|
+
return {}
|
|
68
|
+
normalized: dict[str, Any] = {}
|
|
69
|
+
for key in (
|
|
70
|
+
"takeaway",
|
|
71
|
+
"claim_update",
|
|
72
|
+
"baseline_relation",
|
|
73
|
+
"comparability",
|
|
74
|
+
"failure_mode",
|
|
75
|
+
"next_action",
|
|
76
|
+
):
|
|
77
|
+
raw = value.get(key)
|
|
78
|
+
text = str(raw).strip() if raw is not None else ""
|
|
79
|
+
if text:
|
|
80
|
+
normalized[key] = text
|
|
81
|
+
return normalized
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _evaluation_summary_fields(value: object, *, prefix: str = "Evaluation") -> list[dict[str, Any]]:
|
|
85
|
+
summary = _evaluation_summary(value)
|
|
86
|
+
labels = (
|
|
87
|
+
("takeaway", f"{prefix} Takeaway"),
|
|
88
|
+
("claim_update", f"{prefix} Claim Update"),
|
|
89
|
+
("baseline_relation", f"{prefix} Baseline Relation"),
|
|
90
|
+
("comparability", f"{prefix} Comparability"),
|
|
91
|
+
("failure_mode", f"{prefix} Failure Mode"),
|
|
92
|
+
("next_action", f"{prefix} Next Action"),
|
|
93
|
+
)
|
|
94
|
+
return [_field(label, summary[key]) for key, label in labels if summary.get(key)]
|
|
95
|
+
|
|
96
|
+
|
|
65
97
|
def _artifact_sort_key(item: dict[str, Any]) -> tuple[str, str]:
|
|
66
98
|
payload = item.get("payload") if isinstance(item.get("payload"), dict) else {}
|
|
67
99
|
return (
|
|
@@ -814,6 +846,9 @@ class QuestStageViewBuilder:
|
|
|
814
846
|
)
|
|
815
847
|
latest_metrics_summary = latest_experiment_payload.get("metrics_summary") or latest_result_payload.get("metrics_summary") or {}
|
|
816
848
|
latest_run_id = str(latest_experiment_payload.get("run_id") or "").strip() or None
|
|
849
|
+
latest_evaluation_summary = _evaluation_summary(
|
|
850
|
+
latest_experiment_payload.get("evaluation_summary") or latest_result_payload.get("evaluation_summary")
|
|
851
|
+
)
|
|
817
852
|
|
|
818
853
|
analysis_manifests = self._analysis_manifests()
|
|
819
854
|
analysis_manifest = next(
|
|
@@ -883,6 +918,7 @@ class QuestStageViewBuilder:
|
|
|
883
918
|
_field("Latest Metrics", latest_metrics_summary or "Not recorded"),
|
|
884
919
|
_field("Delta vs Baseline", latest_progress_eval.get("delta_vs_baseline") or "Not recorded"),
|
|
885
920
|
_field("Breakthrough", latest_progress_eval.get("breakthrough_level") or "Not recorded"),
|
|
921
|
+
*_evaluation_summary_fields(latest_evaluation_summary),
|
|
886
922
|
],
|
|
887
923
|
key_files=self._dedupe_files(
|
|
888
924
|
[
|
|
@@ -940,6 +976,7 @@ class QuestStageViewBuilder:
|
|
|
940
976
|
"verdict": latest_experiment_payload.get("verdict"),
|
|
941
977
|
"metrics_summary": latest_metrics_summary,
|
|
942
978
|
"progress_eval": latest_progress_eval,
|
|
979
|
+
"evaluation_summary": latest_evaluation_summary,
|
|
943
980
|
"run_md_path": latest_experiment_paths.get("run_md"),
|
|
944
981
|
"result_json_path": latest_experiment_paths.get("result_json"),
|
|
945
982
|
}
|
|
@@ -979,6 +1016,7 @@ class QuestStageViewBuilder:
|
|
|
979
1016
|
result_payload = read_json(Path(paths.get("result_json")), {}) if str(paths.get("result_json") or "").strip() else {}
|
|
980
1017
|
progress_eval = payload.get("progress_eval") or result_payload.get("progress_eval") or {}
|
|
981
1018
|
baseline_ref = payload.get("baseline_ref") or result_payload.get("baseline_ref") or {}
|
|
1019
|
+
evaluation_summary = _evaluation_summary(payload.get("evaluation_summary") or result_payload.get("evaluation_summary"))
|
|
982
1020
|
run_id = str(payload.get("run_id") or "pending").strip() or "pending"
|
|
983
1021
|
note = (
|
|
984
1022
|
str(payload.get("summary") or result_payload.get("conclusion") or (progress_eval or {}).get("reason") or "").strip()
|
|
@@ -1028,6 +1066,7 @@ class QuestStageViewBuilder:
|
|
|
1028
1066
|
_field("Metrics Summary", metrics_summary or "Not recorded"),
|
|
1029
1067
|
_field("Delta vs Baseline", (progress_eval or {}).get("delta_vs_baseline") or "Not recorded"),
|
|
1030
1068
|
_field("Breakthrough Level", (progress_eval or {}).get("breakthrough_level") or "Not recorded"),
|
|
1069
|
+
*_evaluation_summary_fields(evaluation_summary),
|
|
1031
1070
|
],
|
|
1032
1071
|
key_files=key_files,
|
|
1033
1072
|
history=self._artifact_history(experiment_items),
|
|
@@ -1040,6 +1079,7 @@ class QuestStageViewBuilder:
|
|
|
1040
1079
|
"baseline_ref": baseline_ref,
|
|
1041
1080
|
"metrics_summary": metrics_summary,
|
|
1042
1081
|
"progress_eval": progress_eval,
|
|
1082
|
+
"evaluation_summary": evaluation_summary,
|
|
1043
1083
|
"result_payload": result_payload,
|
|
1044
1084
|
}
|
|
1045
1085
|
},
|
|
@@ -1141,6 +1181,9 @@ class QuestStageViewBuilder:
|
|
|
1141
1181
|
"reviewer_resolution": detail_payload.get("reviewer_resolution"),
|
|
1142
1182
|
"manuscript_update_hint": detail_payload.get("manuscript_update_hint"),
|
|
1143
1183
|
"next_recommendation": detail_payload.get("next_recommendation"),
|
|
1184
|
+
"evaluation_summary": _evaluation_summary(
|
|
1185
|
+
run_payload.get("evaluation_summary") or detail_payload.get("evaluation_summary")
|
|
1186
|
+
),
|
|
1144
1187
|
"deviations": detail_payload.get("deviations") or [],
|
|
1145
1188
|
"evidence_paths": detail_payload.get("evidence_paths") or [],
|
|
1146
1189
|
"plan_path": item.get("plan_path"),
|
|
@@ -1233,8 +1276,11 @@ class QuestStageViewBuilder:
|
|
|
1233
1276
|
self._file_entry("paper/writing_plan.md", label="Writing Plan", description="Paper writing plan."),
|
|
1234
1277
|
self._file_entry("paper/references.bib", label="References", description="Bibliography file."),
|
|
1235
1278
|
self._file_entry("paper/claim_evidence_map.json", label="Claim-Evidence Map", description="Claim to evidence mapping."),
|
|
1279
|
+
self._file_entry("paper/baseline_inventory.json", label="Baseline Inventory", description="Canonical and supplementary baseline inventory for writing."),
|
|
1236
1280
|
self._file_entry("paper/build/compile_report.json", label="Compile Report", description="Paper build/compile report."),
|
|
1237
1281
|
self._file_entry("paper/paper_bundle_manifest.json", label="Bundle Manifest", description="Final paper bundle manifest."),
|
|
1282
|
+
self._file_entry("release/open_source/manifest.json", label="Open Source Manifest", description="Open-source cleanup and release preparation manifest."),
|
|
1283
|
+
self._file_entry("release/open_source/cleanup_plan.md", label="Open Source Cleanup Plan", description="Checklist for cleaning the paper branch into a public release."),
|
|
1238
1284
|
self._file_entry(latex_root_rel, label="LaTeX Sources", description="LaTeX source folder.", expected_kind="directory"),
|
|
1239
1285
|
self._file_entry(main_tex_rel, label="Main TeX", description="Primary TeX source file."),
|
|
1240
1286
|
]
|
|
@@ -530,6 +530,7 @@ class CodexRunner:
|
|
|
530
530
|
|
|
531
531
|
env = dict(**os.environ)
|
|
532
532
|
env["CODEX_HOME"] = str(codex_home)
|
|
533
|
+
env["DEEPSCIENTIST_HOME"] = str(self.home)
|
|
533
534
|
env["DS_HOME"] = str(self.home)
|
|
534
535
|
env["DS_QUEST_ID"] = request.quest_id
|
|
535
536
|
env["DS_QUEST_ROOT"] = str(request.quest_root)
|
|
@@ -846,6 +847,7 @@ class CodexRunner:
|
|
|
846
847
|
tool_timeout_sec = None
|
|
847
848
|
|
|
848
849
|
shared_env = {
|
|
850
|
+
"DEEPSCIENTIST_HOME": str(self.home),
|
|
849
851
|
"DS_HOME": str(self.home),
|
|
850
852
|
"DS_QUEST_ID": quest_id,
|
|
851
853
|
"DS_QUEST_ROOT": str(quest_root),
|
|
@@ -71,7 +71,8 @@ def write_json(path: Path, payload: Any) -> None:
|
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def read_json(path: Path, default: Any = None) -> Any:
|
|
74
|
+
def read_json(path: Path | str, default: Any = None) -> Any:
|
|
75
|
+
path = Path(path)
|
|
75
76
|
if not path.exists():
|
|
76
77
|
return default
|
|
77
78
|
payload = path.read_text(encoding="utf-8").strip()
|
|
@@ -155,35 +156,61 @@ def which(binary: str) -> str | None:
|
|
|
155
156
|
return shutil.which(binary)
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
def
|
|
159
|
-
normalized = str(
|
|
159
|
+
def _resolve_executable_reference(reference: str) -> str | None:
|
|
160
|
+
normalized = str(reference or "").strip()
|
|
160
161
|
if not normalized:
|
|
161
162
|
return None
|
|
162
163
|
|
|
163
164
|
candidate = Path(normalized).expanduser()
|
|
164
165
|
if candidate.is_absolute() or os.path.sep in normalized or (os.path.altsep and os.path.altsep in normalized):
|
|
165
166
|
return str(candidate) if candidate.exists() else None
|
|
167
|
+
return shutil.which(normalized)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _codex_repo_roots() -> list[Path]:
|
|
171
|
+
roots: list[Path] = []
|
|
172
|
+
configured = str(os.environ.get("DEEPSCIENTIST_REPO_ROOT") or "").strip()
|
|
173
|
+
if configured:
|
|
174
|
+
roots.append(Path(configured).expanduser().resolve())
|
|
175
|
+
roots.append(Path(__file__).resolve().parents[2])
|
|
176
|
+
|
|
177
|
+
deduped: list[Path] = []
|
|
178
|
+
seen: set[str] = set()
|
|
179
|
+
for root in roots:
|
|
180
|
+
key = str(root)
|
|
181
|
+
if key in seen:
|
|
182
|
+
continue
|
|
183
|
+
seen.add(key)
|
|
184
|
+
deduped.append(root)
|
|
185
|
+
return deduped
|
|
166
186
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
187
|
+
|
|
188
|
+
def resolve_runner_binary(binary: str, *, runner_name: str | None = None) -> str | None:
|
|
189
|
+
normalized = str(binary or "").strip()
|
|
190
|
+
if not normalized:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
resolved_reference = _resolve_executable_reference(normalized)
|
|
194
|
+
candidate = Path(normalized).expanduser()
|
|
195
|
+
if candidate.is_absolute() or os.path.sep in normalized or (os.path.altsep and os.path.altsep in normalized):
|
|
196
|
+
return resolved_reference
|
|
170
197
|
|
|
171
198
|
normalized_runner = str(runner_name or candidate.name or normalized).strip().lower()
|
|
172
199
|
if normalized_runner != "codex":
|
|
173
|
-
return
|
|
200
|
+
return resolved_reference
|
|
174
201
|
|
|
175
202
|
for env_name in ("DEEPSCIENTIST_CODEX_BINARY", "DS_CODEX_BINARY"):
|
|
176
203
|
override = os.environ.get(env_name)
|
|
177
204
|
if override:
|
|
178
|
-
|
|
179
|
-
if
|
|
180
|
-
return
|
|
205
|
+
resolved_override = _resolve_executable_reference(override)
|
|
206
|
+
if resolved_override:
|
|
207
|
+
return resolved_override
|
|
181
208
|
|
|
182
|
-
repo_root = Path(__file__).resolve().parents[2]
|
|
183
|
-
node_bin_root = repo_root / "node_modules" / ".bin"
|
|
184
209
|
names = ["codex.cmd", "codex.exe", "codex"] if sys.platform.startswith("win") else ["codex"]
|
|
185
|
-
for
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
for root in _codex_repo_roots():
|
|
211
|
+
node_bin_root = root / "node_modules" / ".bin"
|
|
212
|
+
for name in names:
|
|
213
|
+
package_local = node_bin_root / name
|
|
214
|
+
if package_local.exists():
|
|
215
|
+
return str(package_local)
|
|
216
|
+
return resolved_reference
|
|
@@ -10,3 +10,6 @@
|
|
|
10
10
|
- lingzhu_progress_rule: for long-running work, your first substantive reply should contain either the direct answer or the first concrete checkpoint, not a duplicate transport acknowledgement
|
|
11
11
|
- lingzhu_safety_rule: request only actions that are clearly justified by the current quest and understandable to the human user
|
|
12
12
|
- lingzhu_text_rule: even when requesting `surface_actions`, always include a clear text explanation of what is happening and why
|
|
13
|
+
- lingzhu_reply_style_rule: for Lingzhu-facing user-visible text sent through `artifact.interact(...)`, keep the message clear, concise, respectful, and high-information-density
|
|
14
|
+
- lingzhu_reply_length_rule: for each Lingzhu-facing `artifact.interact(...)` message, normally answer in at most 2 to 3 sentences unless the user explicitly asks for more detail
|
|
15
|
+
- lingzhu_summary_first_rule: in Lingzhu-facing `artifact.interact(...)` messages, usually give only the synopsis and key facts needed for the user's next decision or understanding; avoid long preambles, repetition, and low-signal detail
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
- connector_contract_scope: loaded only when QQ is the active or bound external connector for this quest
|
|
5
5
|
- connector_contract_goal: use `artifact.interact(...)` as the main durable user-visible thread on QQ instead of exposing raw internal runner or tool chatter
|
|
6
6
|
- qq_reply_style: keep QQ replies concise, milestone-first, respectful, and easy to scan on a phone
|
|
7
|
+
- qq_reply_length_rule: for ordinary QQ progress updates, normally use only 2 to 4 short sentences, or 3 short bullets at most
|
|
8
|
+
- qq_summary_first_rule: start with the conclusion the user cares about, then what it means, then the next action
|
|
9
|
+
- qq_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete measure explicit whenever possible
|
|
10
|
+
- qq_eta_rule: for baseline reproduction, main experiments, analysis experiments, and other important long-running research phases, include a rough ETA for the next meaningful result or the next update; if uncertain, say that and still give the next check-in window
|
|
11
|
+
- qq_tool_call_keepalive_rule: for ordinary active work, if roughly 10 to 30 tool calls pass without a user-visible checkpoint, send one concise QQ progress update before continuing
|
|
12
|
+
- qq_internal_detail_rule: omit worker names, heartbeat timestamps, retry counters, pending/running/completed counts, file names, and monitor-window narration unless the user asked for them or the detail changes the recommended action
|
|
13
|
+
- qq_translation_rule: convert internal execution and file-management work into user value, such as saying the baseline record is now organized for easier later comparison instead of listing touched files
|
|
14
|
+
- qq_preflight_rule: before sending a QQ progress update, rewrite it if it still sounds like a monitoring log, execution diary, or file inventory
|
|
7
15
|
- qq_operator_surface_rule: treat QQ as an operator surface for coordination and milestone delivery, not as a full artifact browser
|
|
8
16
|
- qq_default_text_rule: plain text is the default and safest QQ mode
|
|
9
17
|
- qq_absolute_path_rule: when you request native QQ image or file delivery via an attachment `path`, prefer an absolute path
|
|
@@ -39,12 +47,44 @@
|
|
|
39
47
|
|
|
40
48
|
## Examples
|
|
41
49
|
|
|
50
|
+
### 0. Bad vs good QQ progress update
|
|
51
|
+
|
|
52
|
+
Bad:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
我刚结束新的 60 秒监控窗,当前还是 15 pending / 2 running / 3 completed。local-gptoss + tare + GSM8K_DSPy 的 heartbeat 已推进到 00:07:10 UTC,local-qwen + atare + BBH_tracking_shuffled_objects_five_objects 也推进到 00:06:38 UTC。我已经同步更新 status、summary、execution 和 inventory,接下来继续看下一段 120 秒恢复窗。
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Why bad:
|
|
59
|
+
|
|
60
|
+
- it forces the user to infer the conclusion from telemetry
|
|
61
|
+
- it exposes internal counters, timestamps, worker labels, and file actions that usually do not help the user
|
|
62
|
+
- it reads like a monitoring transcript, not like a collaborator update
|
|
63
|
+
|
|
64
|
+
Good:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
公开 baseline 还在继续推进,暂时不需要额外修补。当前主要情况是整体在往前走,但其中一条线仍然更慢、更不稳定。接下来我会继续盯下一轮结果,预计 20 到 30 分钟内会有下一次关键判断;如果更早出现完成、再次卡住,或者需要干预,我会提前同步给您。
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Why good:
|
|
71
|
+
|
|
72
|
+
- it starts with the conclusion the user actually needs
|
|
73
|
+
- it keeps the meaningful risk but removes unnecessary internal telemetry
|
|
74
|
+
- it tells the user exactly what will happen next
|
|
75
|
+
|
|
76
|
+
English-style reference shape:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
I'm working on {current task}. The main issue right now is {difficulty or risk}, but {latest real progress or current judgment}. Next I'll {concrete next measure}. You should hear from me again in about {ETA}, or sooner if {important condition} happens.
|
|
80
|
+
```
|
|
81
|
+
|
|
42
82
|
### 1. Plain-text QQ progress update
|
|
43
83
|
|
|
44
84
|
```python
|
|
45
85
|
artifact.interact(
|
|
46
86
|
kind="progress",
|
|
47
|
-
message="
|
|
87
|
+
message="主实验第一轮已经跑完,结果目前比较稳定。接下来我会继续补消融,确认这个提升是不是稳得住。下一次我只同步关键变化给您。",
|
|
48
88
|
reply_mode="threaded",
|
|
49
89
|
)
|
|
50
90
|
```
|
|
@@ -56,7 +96,7 @@ Use the normal `artifact.interact(...)` call. When DeepScientist already knows t
|
|
|
56
96
|
```python
|
|
57
97
|
artifact.interact(
|
|
58
98
|
kind="progress",
|
|
59
|
-
message="
|
|
99
|
+
message="我已经看完您刚才提到的那篇论文,也确认了它和当前 baseline 的核心差异。接下来我会把真正影响路线选择的部分整理出来,再给您一个更完整的结论。",
|
|
60
100
|
reply_mode="threaded",
|
|
61
101
|
)
|
|
62
102
|
```
|