@researai/deepscientist 1.5.8 → 1.5.11
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/LICENSE +186 -21
- package/README.md +108 -95
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +172 -13
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +308 -70
- package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +41 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +79 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +315 -74
- package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +41 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +126 -0
- package/install.sh +0 -34
- package/package.json +3 -3
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/metrics.py +1 -3
- package/src/deepscientist/artifact/service.py +1347 -111
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/service.py +9 -0
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/qq.py +1 -1
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +7 -1
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +317 -0
- package/src/deepscientist/config/models.py +22 -2
- package/src/deepscientist/config/service.py +431 -60
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +295 -5
- package/src/deepscientist/daemon/api/router.py +16 -1
- package/src/deepscientist/daemon/app.py +1130 -61
- package/src/deepscientist/doctor.py +5 -2
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +14 -5
- package/src/deepscientist/prompts/builder.py +29 -1
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +152 -2
- package/src/deepscientist/quest/service.py +169 -43
- package/src/deepscientist/quest/stage_views.py +172 -9
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +55 -3
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/weixin.md +230 -0
- package/src/prompts/system.md +9 -0
- package/src/skills/idea/SKILL.md +16 -0
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/write/SKILL.md +1 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
- package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
- package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
- package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
- package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
- package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
- package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
- package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
- package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
- package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
- package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
- package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
- package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
- package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
- package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
- package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
- package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
- package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
- package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
|
@@ -1,196 +1 @@
|
|
|
1
|
-
from
|
|
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
|
-
def default_qq_profile() -> dict[str, Any]:
|
|
11
|
-
return {
|
|
12
|
-
"profile_id": None,
|
|
13
|
-
"enabled": True,
|
|
14
|
-
"app_id": None,
|
|
15
|
-
"app_secret": None,
|
|
16
|
-
"app_secret_env": None,
|
|
17
|
-
"bot_name": "DeepScientist",
|
|
18
|
-
"main_chat_id": None,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _as_text(value: Any) -> str | None:
|
|
23
|
-
text = str(value or "").strip()
|
|
24
|
-
return text or None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _normalize_secret_pair(payload: dict[str, Any], direct_key: str, env_key: str) -> None:
|
|
28
|
-
direct = _as_text(payload.get(direct_key))
|
|
29
|
-
env_name = _as_text(payload.get(env_key))
|
|
30
|
-
payload[direct_key] = direct
|
|
31
|
-
payload[env_key] = None if direct else env_name
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _profile_id_seed(*, profile_id: Any, app_id: Any, bot_name: Any, index: int) -> str:
|
|
35
|
-
explicit = _as_text(profile_id)
|
|
36
|
-
if explicit:
|
|
37
|
-
return explicit
|
|
38
|
-
app_text = _as_text(app_id)
|
|
39
|
-
if app_text:
|
|
40
|
-
return f"qq-{app_text}"
|
|
41
|
-
bot_text = slugify(str(bot_name or "").strip(), default="")
|
|
42
|
-
if bot_text:
|
|
43
|
-
return f"{QQ_PROFILE_ID_PREFIX}-{bot_text}"
|
|
44
|
-
return f"{QQ_PROFILE_ID_PREFIX}-{index:03d}"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _unique_profile_id(seed: str, *, used: set[str]) -> str:
|
|
48
|
-
base = slugify(seed, default=QQ_PROFILE_ID_PREFIX)
|
|
49
|
-
candidate = base
|
|
50
|
-
suffix = 2
|
|
51
|
-
while candidate in used:
|
|
52
|
-
candidate = f"{base}-{suffix}"
|
|
53
|
-
suffix += 1
|
|
54
|
-
used.add(candidate)
|
|
55
|
-
return candidate
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def list_qq_profiles(config: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
59
|
-
normalized = normalize_qq_connector_config(config)
|
|
60
|
-
profiles = normalized.get("profiles")
|
|
61
|
-
return [dict(item) for item in profiles] if isinstance(profiles, list) else []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def find_qq_profile(
|
|
65
|
-
config: dict[str, Any] | None,
|
|
66
|
-
*,
|
|
67
|
-
profile_id: str | None = None,
|
|
68
|
-
app_id: str | None = None,
|
|
69
|
-
) -> dict[str, Any] | None:
|
|
70
|
-
normalized_profile_id = _as_text(profile_id)
|
|
71
|
-
normalized_app_id = _as_text(app_id)
|
|
72
|
-
for profile in list_qq_profiles(config):
|
|
73
|
-
if normalized_profile_id and str(profile.get("profile_id") or "").strip() == normalized_profile_id:
|
|
74
|
-
return profile
|
|
75
|
-
if normalized_app_id and str(profile.get("app_id") or "").strip() == normalized_app_id:
|
|
76
|
-
return profile
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def merge_qq_profile_config(shared_config: dict[str, Any] | None, profile: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
-
normalized = normalize_qq_connector_config(shared_config)
|
|
82
|
-
merged = deepcopy(normalized)
|
|
83
|
-
merged.pop("profiles", None)
|
|
84
|
-
app_secret = _as_text(profile.get("app_secret"))
|
|
85
|
-
app_secret_env = _as_text(profile.get("app_secret_env"))
|
|
86
|
-
merged.update(
|
|
87
|
-
{
|
|
88
|
-
"profile_id": str(profile.get("profile_id") or "").strip() or None,
|
|
89
|
-
"app_id": _as_text(profile.get("app_id")),
|
|
90
|
-
"app_secret": app_secret,
|
|
91
|
-
"app_secret_env": None if app_secret else app_secret_env,
|
|
92
|
-
"bot_name": _as_text(profile.get("bot_name")) or str(normalized.get("bot_name") or "DeepScientist"),
|
|
93
|
-
"main_chat_id": _as_text(profile.get("main_chat_id")),
|
|
94
|
-
"enabled": bool(normalized.get("enabled", False)) and bool(profile.get("enabled", True)),
|
|
95
|
-
"transport": "gateway_direct",
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
return merged
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def qq_profile_label(profile: dict[str, Any] | None) -> str:
|
|
102
|
-
if not isinstance(profile, dict):
|
|
103
|
-
return "QQ"
|
|
104
|
-
bot_name = _as_text(profile.get("bot_name"))
|
|
105
|
-
app_id = _as_text(profile.get("app_id"))
|
|
106
|
-
if bot_name and app_id:
|
|
107
|
-
return f"{bot_name} · {app_id}"
|
|
108
|
-
if bot_name:
|
|
109
|
-
return bot_name
|
|
110
|
-
if app_id:
|
|
111
|
-
return f"QQ · {app_id}"
|
|
112
|
-
return "QQ"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def normalize_qq_connector_config(config: dict[str, Any] | None) -> dict[str, Any]:
|
|
116
|
-
payload = deepcopy(config or {})
|
|
117
|
-
shared_defaults = {
|
|
118
|
-
"enabled": False,
|
|
119
|
-
"transport": "gateway_direct",
|
|
120
|
-
"app_id": None,
|
|
121
|
-
"app_secret": None,
|
|
122
|
-
"app_secret_env": None,
|
|
123
|
-
"bot_name": "DeepScientist",
|
|
124
|
-
"command_prefix": "/",
|
|
125
|
-
"main_chat_id": None,
|
|
126
|
-
"require_at_in_groups": True,
|
|
127
|
-
"auto_bind_dm_to_active_quest": True,
|
|
128
|
-
"gateway_restart_on_config_change": True,
|
|
129
|
-
"auto_send_main_experiment_png": True,
|
|
130
|
-
"auto_send_analysis_summary_png": True,
|
|
131
|
-
"auto_send_slice_png": True,
|
|
132
|
-
"auto_send_paper_pdf": True,
|
|
133
|
-
"enable_markdown_send": False,
|
|
134
|
-
"enable_file_upload_experimental": False,
|
|
135
|
-
"profiles": [],
|
|
136
|
-
}
|
|
137
|
-
shared = {**shared_defaults, **payload}
|
|
138
|
-
shared["transport"] = "gateway_direct"
|
|
139
|
-
shared["command_prefix"] = _as_text(shared.get("command_prefix")) or "/"
|
|
140
|
-
shared["bot_name"] = _as_text(shared.get("bot_name")) or "DeepScientist"
|
|
141
|
-
_normalize_secret_pair(shared, "app_secret", "app_secret_env")
|
|
142
|
-
|
|
143
|
-
raw_profiles = payload.get("profiles")
|
|
144
|
-
items = list(raw_profiles) if isinstance(raw_profiles, list) else []
|
|
145
|
-
legacy_profile_seed = {
|
|
146
|
-
"app_id": payload.get("app_id"),
|
|
147
|
-
"app_secret": payload.get("app_secret"),
|
|
148
|
-
"app_secret_env": payload.get("app_secret_env"),
|
|
149
|
-
"bot_name": payload.get("bot_name"),
|
|
150
|
-
"main_chat_id": payload.get("main_chat_id"),
|
|
151
|
-
}
|
|
152
|
-
if not items:
|
|
153
|
-
has_direct_profile_seed = any(_as_text(legacy_profile_seed.get(key)) for key in ("app_id", "app_secret", "main_chat_id"))
|
|
154
|
-
has_env_profile_seed = bool(payload.get("enabled")) and bool(_as_text(legacy_profile_seed.get("app_secret_env")))
|
|
155
|
-
if has_direct_profile_seed or has_env_profile_seed:
|
|
156
|
-
items = [legacy_profile_seed]
|
|
157
|
-
|
|
158
|
-
profiles: list[dict[str, Any]] = []
|
|
159
|
-
used_ids: set[str] = set()
|
|
160
|
-
for index, raw in enumerate(items, start=1):
|
|
161
|
-
if not isinstance(raw, dict):
|
|
162
|
-
continue
|
|
163
|
-
current = {**default_qq_profile(), **raw}
|
|
164
|
-
current["enabled"] = bool(current.get("enabled", True))
|
|
165
|
-
current["app_id"] = _as_text(current.get("app_id"))
|
|
166
|
-
current["app_secret"] = _as_text(current.get("app_secret"))
|
|
167
|
-
current["app_secret_env"] = _as_text(current.get("app_secret_env")) or shared["app_secret_env"]
|
|
168
|
-
_normalize_secret_pair(current, "app_secret", "app_secret_env")
|
|
169
|
-
current["bot_name"] = _as_text(current.get("bot_name")) or shared["bot_name"]
|
|
170
|
-
current["main_chat_id"] = _as_text(current.get("main_chat_id"))
|
|
171
|
-
current["profile_id"] = _unique_profile_id(
|
|
172
|
-
_profile_id_seed(
|
|
173
|
-
profile_id=current.get("profile_id"),
|
|
174
|
-
app_id=current.get("app_id"),
|
|
175
|
-
bot_name=current.get("bot_name"),
|
|
176
|
-
index=index,
|
|
177
|
-
),
|
|
178
|
-
used=used_ids,
|
|
179
|
-
)
|
|
180
|
-
profiles.append(current)
|
|
181
|
-
|
|
182
|
-
shared["profiles"] = profiles
|
|
183
|
-
if len(profiles) == 1:
|
|
184
|
-
mirror = profiles[0]
|
|
185
|
-
shared["app_id"] = mirror.get("app_id")
|
|
186
|
-
shared["app_secret"] = mirror.get("app_secret")
|
|
187
|
-
shared["app_secret_env"] = mirror.get("app_secret_env")
|
|
188
|
-
shared["bot_name"] = mirror.get("bot_name")
|
|
189
|
-
shared["main_chat_id"] = mirror.get("main_chat_id")
|
|
190
|
-
else:
|
|
191
|
-
shared["app_id"] = None
|
|
192
|
-
shared["app_secret"] = None
|
|
193
|
-
shared["app_secret_env"] = None
|
|
194
|
-
shared["main_chat_id"] = None
|
|
195
|
-
|
|
196
|
-
return shared
|
|
1
|
+
from .connector.qq_profiles import * # noqa: F401,F403
|
|
@@ -6,6 +6,8 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from ..shared import ensure_dir, read_json, sha256_text, utc_now, write_json
|
|
8
8
|
|
|
9
|
+
NODE_TRACE_SCHEMA_VERSION = 2
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def _format_state_label(value: str | None) -> str:
|
|
11
13
|
normalized = str(value or "").strip().replace("_", " ").replace("-", " ")
|
|
@@ -36,6 +38,23 @@ def _normalize_branch_name(value: object, *, fallback: str) -> str:
|
|
|
36
38
|
return text or fallback
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _infer_stage_from_branch_name(value: object) -> str | None:
|
|
42
|
+
normalized = str(value or "").strip().lower()
|
|
43
|
+
if not normalized:
|
|
44
|
+
return None
|
|
45
|
+
if normalized.startswith("analysis/"):
|
|
46
|
+
return "analysis"
|
|
47
|
+
if normalized.startswith("run/"):
|
|
48
|
+
return "experiment"
|
|
49
|
+
if normalized.startswith("idea/"):
|
|
50
|
+
return "idea"
|
|
51
|
+
if normalized.startswith("paper/") or normalized.startswith("write/"):
|
|
52
|
+
return "writing"
|
|
53
|
+
if normalized.startswith("baseline/"):
|
|
54
|
+
return "baseline"
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
39
58
|
def _infer_stage_from_skill(skill_id: object) -> str | None:
|
|
40
59
|
normalized = str(skill_id or "").strip().lower()
|
|
41
60
|
if not normalized:
|
|
@@ -46,8 +65,10 @@ def _infer_stage_from_skill(skill_id: object) -> str | None:
|
|
|
46
65
|
return "baseline"
|
|
47
66
|
if normalized in {"idea", "scout+idea"}:
|
|
48
67
|
return "idea"
|
|
49
|
-
if normalized
|
|
68
|
+
if normalized == "experiment":
|
|
50
69
|
return "experiment"
|
|
70
|
+
if normalized in {"analysis-campaign", "analysis", "analysis_slice"}:
|
|
71
|
+
return "analysis"
|
|
51
72
|
if normalized in {"write", "finalize"}:
|
|
52
73
|
return "writing"
|
|
53
74
|
if normalized == "decision":
|
|
@@ -103,6 +124,56 @@ def _load_artifact_record(path: Path) -> dict[str, Any] | None:
|
|
|
103
124
|
return None
|
|
104
125
|
|
|
105
126
|
|
|
127
|
+
def _normalize_paths_map(value: object) -> dict[str, str | None]:
|
|
128
|
+
if not isinstance(value, dict):
|
|
129
|
+
return {}
|
|
130
|
+
normalized: dict[str, str | None] = {}
|
|
131
|
+
for raw_key, raw_value in value.items():
|
|
132
|
+
key = str(raw_key or "").strip()
|
|
133
|
+
if not key:
|
|
134
|
+
continue
|
|
135
|
+
if raw_value is None:
|
|
136
|
+
normalized[key] = None
|
|
137
|
+
continue
|
|
138
|
+
text = str(raw_value).strip()
|
|
139
|
+
normalized[key] = text or None
|
|
140
|
+
return normalized
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _normalize_string_list(value: object) -> list[str]:
|
|
144
|
+
if not isinstance(value, list):
|
|
145
|
+
return []
|
|
146
|
+
items: list[str] = []
|
|
147
|
+
seen: set[str] = set()
|
|
148
|
+
for raw in value:
|
|
149
|
+
text = str(raw or "").strip()
|
|
150
|
+
if not text or text in seen:
|
|
151
|
+
continue
|
|
152
|
+
seen.add(text)
|
|
153
|
+
items.append(text)
|
|
154
|
+
return items
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _run_artifact_context(quest_root: Path, run_id: str | None) -> dict[str, Any] | None:
|
|
158
|
+
normalized = str(run_id or "").strip()
|
|
159
|
+
if not normalized:
|
|
160
|
+
return None
|
|
161
|
+
path = quest_root / ".ds" / "runs" / normalized / "artifact.json"
|
|
162
|
+
payload = read_json(path, {})
|
|
163
|
+
if not isinstance(payload, dict) or not payload:
|
|
164
|
+
return None
|
|
165
|
+
record = dict(payload.get("record") or {}) if isinstance(payload.get("record"), dict) else {}
|
|
166
|
+
checkpoint = dict(payload.get("checkpoint") or {}) if isinstance(payload.get("checkpoint"), dict) else {}
|
|
167
|
+
return {
|
|
168
|
+
"path": str(path),
|
|
169
|
+
"record": record,
|
|
170
|
+
"checkpoint": checkpoint,
|
|
171
|
+
"head_commit": str(checkpoint.get("head") or record.get("head_commit") or "").strip() or None,
|
|
172
|
+
"paths_map": _normalize_paths_map(record.get("paths")),
|
|
173
|
+
"changed_files": _normalize_string_list(record.get("files_changed")),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
106
177
|
def _build_run_contexts(quest_root: Path, *, default_branch: str) -> dict[str, dict[str, Any]]:
|
|
107
178
|
contexts: dict[str, dict[str, Any]] = {}
|
|
108
179
|
artifact_roots = [quest_root / "artifacts"]
|
|
@@ -145,6 +216,7 @@ def _build_run_contexts(quest_root: Path, *, default_branch: str) -> dict[str, d
|
|
|
145
216
|
current["worktree_rel_path"] = current.get("worktree_rel_path") or record.get("worktree_rel_path")
|
|
146
217
|
current["summary"] = current.get("summary") or summary
|
|
147
218
|
current["updated_at"] = current.get("updated_at") or updated_at
|
|
219
|
+
current["artifact_path"] = current.get("artifact_path") or str(artifact_path)
|
|
148
220
|
contexts[run_id] = current
|
|
149
221
|
return contexts
|
|
150
222
|
|
|
@@ -160,6 +232,7 @@ def _resolve_entry_context(
|
|
|
160
232
|
raw_event_type = str(entry.get("raw_event_type") or entry.get("kind") or "").strip()
|
|
161
233
|
run_context = run_contexts.get(run_id or "")
|
|
162
234
|
artifact_context: dict[str, Any] | None = None
|
|
235
|
+
artifact_path: str | None = None
|
|
163
236
|
for raw_path in entry.get("paths") or []:
|
|
164
237
|
try:
|
|
165
238
|
path = Path(str(raw_path))
|
|
@@ -169,16 +242,24 @@ def _resolve_entry_context(
|
|
|
169
242
|
continue
|
|
170
243
|
artifact_context = _load_artifact_record(path)
|
|
171
244
|
if artifact_context:
|
|
245
|
+
artifact_path = str(path)
|
|
172
246
|
break
|
|
247
|
+
if run_id is None and artifact_context:
|
|
248
|
+
run_id = str(artifact_context.get("run_id") or "").strip() or None
|
|
249
|
+
run_artifact = _run_artifact_context(quest_root, run_id)
|
|
173
250
|
branch_name = _normalize_branch_name(
|
|
174
|
-
(artifact_context or {}).get("branch")
|
|
251
|
+
(artifact_context or {}).get("branch")
|
|
252
|
+
or (((run_artifact or {}).get("record") or {}) if isinstance((run_artifact or {}).get("record"), dict) else {}).get("branch")
|
|
253
|
+
or (run_context or {}).get("branch_name"),
|
|
175
254
|
fallback=default_branch,
|
|
176
255
|
)
|
|
177
256
|
stage_key = (
|
|
178
257
|
_infer_stage_from_skill(entry.get("skill_id"))
|
|
179
258
|
or _infer_stage_from_artifact(artifact_context or {})
|
|
259
|
+
or _infer_stage_from_artifact((((run_artifact or {}).get("record") or {}) if run_artifact else {}))
|
|
180
260
|
or (run_context or {}).get("stage_key")
|
|
181
261
|
or _infer_stage_from_event_type(raw_event_type)
|
|
262
|
+
or _infer_stage_from_branch_name(branch_name)
|
|
182
263
|
or "general"
|
|
183
264
|
)
|
|
184
265
|
return {
|
|
@@ -186,9 +267,15 @@ def _resolve_entry_context(
|
|
|
186
267
|
"branch_name": branch_name,
|
|
187
268
|
"stage_key": stage_key,
|
|
188
269
|
"worktree_rel_path": (artifact_context or {}).get("worktree_rel_path")
|
|
270
|
+
or (((run_artifact or {}).get("record") or {}) if run_artifact else {}).get("worktree_rel_path")
|
|
189
271
|
or (run_context or {}).get("worktree_rel_path"),
|
|
272
|
+
"artifact_context": artifact_context,
|
|
273
|
+
"artifact_path": artifact_path or (run_context or {}).get("artifact_path"),
|
|
274
|
+
"run_artifact": run_artifact,
|
|
190
275
|
"trace_confidence": "artifact"
|
|
191
276
|
if artifact_context
|
|
277
|
+
else "run_artifact"
|
|
278
|
+
if run_artifact
|
|
192
279
|
else "run_context"
|
|
193
280
|
if run_context
|
|
194
281
|
else "default_branch",
|
|
@@ -196,6 +283,25 @@ def _resolve_entry_context(
|
|
|
196
283
|
|
|
197
284
|
|
|
198
285
|
def _build_action(entry: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
|
286
|
+
artifact_context = (
|
|
287
|
+
dict(context.get("artifact_context") or {})
|
|
288
|
+
if isinstance(context.get("artifact_context"), dict)
|
|
289
|
+
else {}
|
|
290
|
+
)
|
|
291
|
+
run_artifact = (
|
|
292
|
+
dict(context.get("run_artifact") or {})
|
|
293
|
+
if isinstance(context.get("run_artifact"), dict)
|
|
294
|
+
else {}
|
|
295
|
+
)
|
|
296
|
+
run_record = dict(run_artifact.get("record") or {}) if isinstance(run_artifact.get("record"), dict) else {}
|
|
297
|
+
paths_map = _normalize_paths_map(artifact_context.get("paths")) or _normalize_paths_map(run_record.get("paths"))
|
|
298
|
+
changed_files = _normalize_string_list(artifact_context.get("files_changed")) or _normalize_string_list(
|
|
299
|
+
artifact_context.get("changed_files")
|
|
300
|
+
) or _normalize_string_list(run_record.get("files_changed")) or _normalize_string_list(run_artifact.get("changed_files"))
|
|
301
|
+
details_json = dict(artifact_context.get("details") or {}) if isinstance(artifact_context.get("details"), dict) else {}
|
|
302
|
+
checkpoint_json = dict(run_artifact.get("checkpoint") or {}) if isinstance(run_artifact.get("checkpoint"), dict) else {}
|
|
303
|
+
metadata = dict(entry.get("metadata") or {}) if isinstance(entry.get("metadata"), dict) else {}
|
|
304
|
+
payload_json = artifact_context or metadata or None
|
|
199
305
|
return {
|
|
200
306
|
"action_id": entry.get("id"),
|
|
201
307
|
"kind": entry.get("kind"),
|
|
@@ -217,6 +323,22 @@ def _build_action(entry: dict[str, Any], context: dict[str, Any]) -> dict[str, A
|
|
|
217
323
|
"reason": entry.get("reason"),
|
|
218
324
|
"raw_event_type": entry.get("raw_event_type"),
|
|
219
325
|
"paths": [str(item) for item in (entry.get("paths") or []) if item],
|
|
326
|
+
"paths_map": paths_map or None,
|
|
327
|
+
"artifact_id": artifact_context.get("artifact_id"),
|
|
328
|
+
"artifact_kind": artifact_context.get("kind"),
|
|
329
|
+
"artifact_path": context.get("artifact_path"),
|
|
330
|
+
"head_commit": (
|
|
331
|
+
str(
|
|
332
|
+
artifact_context.get("head_commit")
|
|
333
|
+
or run_artifact.get("head_commit")
|
|
334
|
+
or ""
|
|
335
|
+
).strip()
|
|
336
|
+
or None
|
|
337
|
+
),
|
|
338
|
+
"payload_json": payload_json,
|
|
339
|
+
"details_json": details_json or (metadata or None),
|
|
340
|
+
"checkpoint_json": checkpoint_json or None,
|
|
341
|
+
"changed_files": changed_files or None,
|
|
220
342
|
"trace_confidence": context.get("trace_confidence"),
|
|
221
343
|
}
|
|
222
344
|
|
|
@@ -277,6 +399,24 @@ def _build_trace_item(
|
|
|
277
399
|
) or None
|
|
278
400
|
run_ids = sorted({str(item.get("run_id") or "").strip() for item in ordered_actions if item.get("run_id")})
|
|
279
401
|
skill_ids = sorted({str(item.get("skill_id") or "").strip() for item in ordered_actions if item.get("skill_id")})
|
|
402
|
+
primary_action = next(
|
|
403
|
+
(
|
|
404
|
+
action
|
|
405
|
+
for action in reversed(ordered_actions)
|
|
406
|
+
if action.get("artifact_id")
|
|
407
|
+
or action.get("head_commit")
|
|
408
|
+
or action.get("payload_json")
|
|
409
|
+
or action.get("details_json")
|
|
410
|
+
),
|
|
411
|
+
ordered_actions[-1] if ordered_actions else {},
|
|
412
|
+
)
|
|
413
|
+
merged_paths_map: dict[str, str | None] = {}
|
|
414
|
+
for action in ordered_actions:
|
|
415
|
+
if isinstance(action.get("paths_map"), dict):
|
|
416
|
+
merged_paths_map.update(action.get("paths_map") or {})
|
|
417
|
+
merged_changed_files = _normalize_string_list(
|
|
418
|
+
[item for action in ordered_actions for item in (action.get("changed_files") or [])]
|
|
419
|
+
)
|
|
280
420
|
return {
|
|
281
421
|
"selection_type": selection_type,
|
|
282
422
|
"selection_ref": selection_ref,
|
|
@@ -291,6 +431,13 @@ def _build_trace_item(
|
|
|
291
431
|
"counts": _build_counts(ordered_actions),
|
|
292
432
|
"run_ids": run_ids,
|
|
293
433
|
"skill_ids": skill_ids,
|
|
434
|
+
"artifact_id": primary_action.get("artifact_id"),
|
|
435
|
+
"artifact_kind": primary_action.get("artifact_kind"),
|
|
436
|
+
"head_commit": primary_action.get("head_commit"),
|
|
437
|
+
"payload_json": primary_action.get("payload_json"),
|
|
438
|
+
"details_json": primary_action.get("details_json"),
|
|
439
|
+
"paths_map": merged_paths_map or None,
|
|
440
|
+
"changed_files": merged_changed_files or None,
|
|
294
441
|
"actions": ordered_actions,
|
|
295
442
|
}
|
|
296
443
|
|
|
@@ -313,6 +460,7 @@ class QuestNodeTraceManager:
|
|
|
313
460
|
source_signature = sha256_text(
|
|
314
461
|
json.dumps(
|
|
315
462
|
{
|
|
463
|
+
"schema_version": NODE_TRACE_SCHEMA_VERSION,
|
|
316
464
|
"entries": workflow.get("entries") or [],
|
|
317
465
|
"branch": (snapshot or {}).get("branch"),
|
|
318
466
|
},
|
|
@@ -324,6 +472,7 @@ class QuestNodeTraceManager:
|
|
|
324
472
|
if (
|
|
325
473
|
isinstance(existing, dict)
|
|
326
474
|
and existing.get("quest_id") == quest_id
|
|
475
|
+
and existing.get("schema_version") == NODE_TRACE_SCHEMA_VERSION
|
|
327
476
|
and existing.get("source_signature") == source_signature
|
|
328
477
|
and isinstance(existing.get("items"), list)
|
|
329
478
|
):
|
|
@@ -418,6 +567,7 @@ class QuestNodeTraceManager:
|
|
|
418
567
|
|
|
419
568
|
payload = {
|
|
420
569
|
"quest_id": quest_id,
|
|
570
|
+
"schema_version": NODE_TRACE_SCHEMA_VERSION,
|
|
421
571
|
"generated_at": utc_now(),
|
|
422
572
|
"source_signature": source_signature,
|
|
423
573
|
"items": items,
|