nexus-agent-cli 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/PKG-INFO +68 -4
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/README.md +65 -1
- nexus_agent_cli-0.7.0/nexus_agent_cli/__init__.py +1 -0
- nexus_agent_cli-0.7.0/nexus_agent_cli/api_client.py +323 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/commands/__init__.py +2 -0
- nexus_agent_cli-0.7.0/nexus_agent_cli/commands/discord/cmd.py +294 -0
- nexus_agent_cli-0.7.0/nexus_agent_cli/commands/run/__init__.py +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/commands/run/cmd.py +82 -4
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/pyproject.toml +2 -2
- nexus_agent_cli-0.7.0/tests/__init__.py +0 -0
- nexus_agent_cli-0.7.0/tests/test_api_client_shapes.py +165 -0
- nexus_agent_cli-0.6.0/nexus_agent_cli/__init__.py +0 -1
- nexus_agent_cli-0.6.0/nexus_agent_cli/api_client.py +0 -169
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/.gitignore +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/__main__.py +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/auth_pkce.py +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/commands/auth/__init__.py +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/commands/auth/cmd.py +0 -0
- {nexus_agent_cli-0.6.0/nexus_agent_cli/commands/run → nexus_agent_cli-0.7.0/nexus_agent_cli/commands/discord}/__init__.py +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/config.py +0 -0
- {nexus_agent_cli-0.6.0 → nexus_agent_cli-0.7.0}/nexus_agent_cli/main.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: nexus-agent-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Nexus internal agent ops CLI — pull run params, push analysis results, send notifications. Admin only.
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Nexus internal agent ops CLI — pull run params, push analysis results, drive the Discord KB / blog-draft data plane, send notifications. Admin only.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: httpx>=0.27
|
|
7
7
|
Requires-Dist: platformdirs>=4.2
|
|
@@ -85,6 +85,42 @@ nexus-agent push-config --ticker tqqq --time-range 2026-06-01_2026-06-14 \
|
|
|
85
85
|
nexus-agent notify --to a@x.com --to b@y.com --subject "Weekly report" --body-file summary.txt
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
### `discord` —— Discord KB / blog draft 数据平面
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 1) 起草前必调:取当前生效的风格 playbook
|
|
92
|
+
# ⚠️ 没有 active playbook 时返回 409 并退出 1 —— 必须中止本次运行,
|
|
93
|
+
# 不允许降级为无风格约束照写
|
|
94
|
+
nexus-agent discord playbook-active --out playbook.json --body-out playbook.md
|
|
95
|
+
|
|
96
|
+
# 2) 取材(窗口由服务端水位线定义,**没有 --date**)
|
|
97
|
+
nexus-agent discord clusters-pending --out clusters.json
|
|
98
|
+
|
|
99
|
+
# 3) 回写富化结果(只改 title/summary/tickers/topics,不碰 blog_state)
|
|
100
|
+
nexus-agent discord push-enrichment --file enrichment.json
|
|
101
|
+
|
|
102
|
+
# 4) 回传草稿(markdown 正文 + 结构化 meta)
|
|
103
|
+
# meta.json: {title, draft_date, source_cluster_ids: [...非空], front_matter?, critic_report?}
|
|
104
|
+
nexus-agent discord push-blog-draft --file draft.md --meta meta.json
|
|
105
|
+
|
|
106
|
+
# 5) 反馈折叠:够 10 条可折叠反馈才跑 playbook_reviser
|
|
107
|
+
nexus-agent discord feedback-pending --out feedback.json
|
|
108
|
+
|
|
109
|
+
# 6) 提交 playbook。meta.json: {rules: [...], change_log, derived_from?, folded_feedback_ids?}
|
|
110
|
+
# ⚠️ 不要在 meta 里放 token_est —— 服务端用 count_tokens 实算(客户端会直接拒绝)
|
|
111
|
+
nexus-agent discord push-playbook --file playbook.md --meta meta.json --activate # v1 才用 --activate
|
|
112
|
+
nexus-agent discord push-playbook --file playbook.md --meta meta.json # 修订版:先落库待审批
|
|
113
|
+
nexus-agent discord activate-playbook --version 2 # 相当于在 #blog-review 点 ✅
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
⚠️ **`clusters-pending` 刻意没有 `--date`。** 取材窗口是「上次成功运行覆盖到的时刻 →
|
|
117
|
+
现在」,不是自然日 —— 按自然日切会出现某天的笔记谁也没收或被收两次。响应里的
|
|
118
|
+
`window` 字段会告诉你这次实际覆盖了哪一段。
|
|
119
|
+
|
|
120
|
+
⚠️ **`push-blog-draft` 目前只落库。** 把草稿发到 `#blog-review` 并挂审批 reaction
|
|
121
|
+
尚未接线,响应里的 `discord_delivery` 会是 `not_wired`。所以 `activate-playbook`
|
|
122
|
+
是目前唯一的"✅"入口。
|
|
123
|
+
|
|
88
124
|
## 配置(环境变量,均可选)
|
|
89
125
|
|
|
90
126
|
| 变量 | 默认 | 说明 |
|
|
@@ -107,15 +143,43 @@ NEXUS_AGENT_API_BASE_URL=http://127.0.0.1:8000/api nexus-agent run-config
|
|
|
107
143
|
`run-config/`、`upload/market-analysis/`、`upload/portfolio-recommendation/`、
|
|
108
144
|
`upload/backtest/`、`notify/`。
|
|
109
145
|
|
|
146
|
+
`discord` 子命令组调用 `apps/discord_kb` 的 7 个 admin 端点
|
|
147
|
+
(`clusters/pending/`、`clusters/enrich/`、`blog-drafts/`、`style-playbook/active/`、
|
|
148
|
+
`style-playbook/`、`style-playbook/<version>/activate/`、`feedback/pending/`)。
|
|
149
|
+
设计与不变量见 nexus-service `docs/DISCORD_KB_AND_BOT_DESIGN.md`(§A.6 是这一组的来源)。
|
|
150
|
+
|
|
151
|
+
## 测试
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python -m unittest discover -s tests
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
只用标准库(`unittest` + `http.server`),不需要额外依赖、不出网、不读凭据目录 ——
|
|
158
|
+
`tests/test_api_client_shapes.py` 把请求打到 127.0.0.1 的捕获服务器上,验路径与
|
|
159
|
+
JSON body 的形状(这类错误在真实调用里只会表现为服务端 400/422,那时已分不清是
|
|
160
|
+
本地拼错还是服务端变了)。
|
|
161
|
+
|
|
110
162
|
## 发布新版本
|
|
111
163
|
|
|
112
164
|
```bash
|
|
113
|
-
# 1.
|
|
165
|
+
# 1. 版本号有**两个**事实源,必须一起改:
|
|
166
|
+
# pyproject.toml 的 version → 决定包版本
|
|
167
|
+
# nexus_agent_cli/__init__.py 的 __version__ → 决定 `nexus-agent version` 打印什么
|
|
168
|
+
# 只改前者的话,装上新版后 `nexus-agent version` 仍打印旧号 —— 不报错、只是骗人。
|
|
169
|
+
# (2026-08-22 发 0.7.0 时就差点这样发出去。)
|
|
114
170
|
# 2. 构建并发布到 PyPI(用 .zshrc 里的 UV_PUBLISH_PASSWORD all-projects token)
|
|
115
171
|
rm -rf dist && python -m build
|
|
172
|
+
python -m twine check dist/* # 先查,再传
|
|
116
173
|
TWINE_USERNAME=__token__ TWINE_PASSWORD="$UV_PUBLISH_PASSWORD" twine upload dist/*
|
|
117
174
|
```
|
|
118
175
|
|
|
176
|
+
发布前建议核一遍(同一版本号 PyPI 不允许重传,错了只能跳版本):
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
grep '^version' pyproject.toml; grep __version__ nexus_agent_cli/__init__.py # 两处一致
|
|
180
|
+
python -c "import zipfile,glob; print(*sorted(zipfile.ZipFile(glob.glob('dist/*.whl')[0]).namelist()),sep=chr(10))"
|
|
181
|
+
```
|
|
182
|
+
|
|
119
183
|
## 迁移说明
|
|
120
184
|
|
|
121
185
|
当前 AWS(EventBridge → ECS Fargate)的每周定时运行**保持不变**。本 CLI 提供并行的
|
|
@@ -74,6 +74,42 @@ nexus-agent push-config --ticker tqqq --time-range 2026-06-01_2026-06-14 \
|
|
|
74
74
|
nexus-agent notify --to a@x.com --to b@y.com --subject "Weekly report" --body-file summary.txt
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### `discord` —— Discord KB / blog draft 数据平面
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# 1) 起草前必调:取当前生效的风格 playbook
|
|
81
|
+
# ⚠️ 没有 active playbook 时返回 409 并退出 1 —— 必须中止本次运行,
|
|
82
|
+
# 不允许降级为无风格约束照写
|
|
83
|
+
nexus-agent discord playbook-active --out playbook.json --body-out playbook.md
|
|
84
|
+
|
|
85
|
+
# 2) 取材(窗口由服务端水位线定义,**没有 --date**)
|
|
86
|
+
nexus-agent discord clusters-pending --out clusters.json
|
|
87
|
+
|
|
88
|
+
# 3) 回写富化结果(只改 title/summary/tickers/topics,不碰 blog_state)
|
|
89
|
+
nexus-agent discord push-enrichment --file enrichment.json
|
|
90
|
+
|
|
91
|
+
# 4) 回传草稿(markdown 正文 + 结构化 meta)
|
|
92
|
+
# meta.json: {title, draft_date, source_cluster_ids: [...非空], front_matter?, critic_report?}
|
|
93
|
+
nexus-agent discord push-blog-draft --file draft.md --meta meta.json
|
|
94
|
+
|
|
95
|
+
# 5) 反馈折叠:够 10 条可折叠反馈才跑 playbook_reviser
|
|
96
|
+
nexus-agent discord feedback-pending --out feedback.json
|
|
97
|
+
|
|
98
|
+
# 6) 提交 playbook。meta.json: {rules: [...], change_log, derived_from?, folded_feedback_ids?}
|
|
99
|
+
# ⚠️ 不要在 meta 里放 token_est —— 服务端用 count_tokens 实算(客户端会直接拒绝)
|
|
100
|
+
nexus-agent discord push-playbook --file playbook.md --meta meta.json --activate # v1 才用 --activate
|
|
101
|
+
nexus-agent discord push-playbook --file playbook.md --meta meta.json # 修订版:先落库待审批
|
|
102
|
+
nexus-agent discord activate-playbook --version 2 # 相当于在 #blog-review 点 ✅
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
⚠️ **`clusters-pending` 刻意没有 `--date`。** 取材窗口是「上次成功运行覆盖到的时刻 →
|
|
106
|
+
现在」,不是自然日 —— 按自然日切会出现某天的笔记谁也没收或被收两次。响应里的
|
|
107
|
+
`window` 字段会告诉你这次实际覆盖了哪一段。
|
|
108
|
+
|
|
109
|
+
⚠️ **`push-blog-draft` 目前只落库。** 把草稿发到 `#blog-review` 并挂审批 reaction
|
|
110
|
+
尚未接线,响应里的 `discord_delivery` 会是 `not_wired`。所以 `activate-playbook`
|
|
111
|
+
是目前唯一的"✅"入口。
|
|
112
|
+
|
|
77
113
|
## 配置(环境变量,均可选)
|
|
78
114
|
|
|
79
115
|
| 变量 | 默认 | 说明 |
|
|
@@ -96,15 +132,43 @@ NEXUS_AGENT_API_BASE_URL=http://127.0.0.1:8000/api nexus-agent run-config
|
|
|
96
132
|
`run-config/`、`upload/market-analysis/`、`upload/portfolio-recommendation/`、
|
|
97
133
|
`upload/backtest/`、`notify/`。
|
|
98
134
|
|
|
135
|
+
`discord` 子命令组调用 `apps/discord_kb` 的 7 个 admin 端点
|
|
136
|
+
(`clusters/pending/`、`clusters/enrich/`、`blog-drafts/`、`style-playbook/active/`、
|
|
137
|
+
`style-playbook/`、`style-playbook/<version>/activate/`、`feedback/pending/`)。
|
|
138
|
+
设计与不变量见 nexus-service `docs/DISCORD_KB_AND_BOT_DESIGN.md`(§A.6 是这一组的来源)。
|
|
139
|
+
|
|
140
|
+
## 测试
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
python -m unittest discover -s tests
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
只用标准库(`unittest` + `http.server`),不需要额外依赖、不出网、不读凭据目录 ——
|
|
147
|
+
`tests/test_api_client_shapes.py` 把请求打到 127.0.0.1 的捕获服务器上,验路径与
|
|
148
|
+
JSON body 的形状(这类错误在真实调用里只会表现为服务端 400/422,那时已分不清是
|
|
149
|
+
本地拼错还是服务端变了)。
|
|
150
|
+
|
|
99
151
|
## 发布新版本
|
|
100
152
|
|
|
101
153
|
```bash
|
|
102
|
-
# 1.
|
|
154
|
+
# 1. 版本号有**两个**事实源,必须一起改:
|
|
155
|
+
# pyproject.toml 的 version → 决定包版本
|
|
156
|
+
# nexus_agent_cli/__init__.py 的 __version__ → 决定 `nexus-agent version` 打印什么
|
|
157
|
+
# 只改前者的话,装上新版后 `nexus-agent version` 仍打印旧号 —— 不报错、只是骗人。
|
|
158
|
+
# (2026-08-22 发 0.7.0 时就差点这样发出去。)
|
|
103
159
|
# 2. 构建并发布到 PyPI(用 .zshrc 里的 UV_PUBLISH_PASSWORD all-projects token)
|
|
104
160
|
rm -rf dist && python -m build
|
|
161
|
+
python -m twine check dist/* # 先查,再传
|
|
105
162
|
TWINE_USERNAME=__token__ TWINE_PASSWORD="$UV_PUBLISH_PASSWORD" twine upload dist/*
|
|
106
163
|
```
|
|
107
164
|
|
|
165
|
+
发布前建议核一遍(同一版本号 PyPI 不允许重传,错了只能跳版本):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
grep '^version' pyproject.toml; grep __version__ nexus_agent_cli/__init__.py # 两处一致
|
|
169
|
+
python -c "import zipfile,glob; print(*sorted(zipfile.ZipFile(glob.glob('dist/*.whl')[0]).namelist()),sep=chr(10))"
|
|
170
|
+
```
|
|
171
|
+
|
|
108
172
|
## 迁移说明
|
|
109
173
|
|
|
110
174
|
当前 AWS(EventBridge → ECS Fargate)的每周定时运行**保持不变**。本 CLI 提供并行的
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from nexus_agent_cli.auth_pkce import ensure_fresh_id_token
|
|
9
|
+
from nexus_agent_cli.config import api_base_url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _headers() -> dict[str, str]:
|
|
13
|
+
token = ensure_fresh_id_token()
|
|
14
|
+
return {"Authorization": f"Bearer {token}", "Accept": "application/json"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _raise_for_error(response: httpx.Response) -> None:
|
|
18
|
+
if response.status_code < 400:
|
|
19
|
+
return
|
|
20
|
+
try:
|
|
21
|
+
detail: Any = response.json()
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
detail = response.text
|
|
24
|
+
raise RuntimeError(f"HTTP {response.status_code}: {detail}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_json(path: str, *, params: dict[str, Any] | None = None, timeout: float = 60.0) -> Any:
|
|
28
|
+
with httpx.Client(timeout=timeout) as client:
|
|
29
|
+
response = client.get(f"{api_base_url()}{path}", headers=_headers(), params=params)
|
|
30
|
+
_raise_for_error(response)
|
|
31
|
+
return response.json()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def post_json(path: str, body: Any, *, timeout: float = 120.0) -> Any:
|
|
35
|
+
headers = {**_headers(), "Content-Type": "application/json"}
|
|
36
|
+
with httpx.Client(timeout=timeout) as client:
|
|
37
|
+
response = client.post(f"{api_base_url()}{path}", headers=headers, json=body)
|
|
38
|
+
_raise_for_error(response)
|
|
39
|
+
return response.json()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Nexus agent endpoints (admin only) ──────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_run_config(tickers: str | None = None) -> Any:
|
|
46
|
+
"""GET /nexus-agent/run-config/ — tickers + date range for this week's run."""
|
|
47
|
+
params = {"tickers": tickers} if tickers else None
|
|
48
|
+
return get_json("/nexus-agent/run-config/", params=params)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_strategy_data(
|
|
52
|
+
ticker: str, start_date: str | None = None, end_date: str | None = None
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""GET /nexus-agent/strategy-data/ — strategy trigger rows (Sheets creds stay server-side)."""
|
|
55
|
+
params: dict[str, Any] = {"ticker": ticker}
|
|
56
|
+
if start_date:
|
|
57
|
+
params["start_date"] = start_date
|
|
58
|
+
if end_date:
|
|
59
|
+
params["end_date"] = end_date
|
|
60
|
+
return get_json("/nexus-agent/strategy-data/", params=params)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_backtest(ticker: str) -> Any:
|
|
64
|
+
"""GET /nexus-agent/backtest/ — latest aggregated backtest data for one ticker."""
|
|
65
|
+
return get_json("/nexus-agent/backtest/", params={"ticker": ticker})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_market_analysis(ticker: str) -> Any:
|
|
69
|
+
"""GET /nexus-agent/market-analysis/ — latest market analysis report for one ticker."""
|
|
70
|
+
return get_json("/nexus-agent/market-analysis/", params={"ticker": ticker})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_portfolio_recommendation() -> Any:
|
|
74
|
+
"""GET /nexus-agent/portfolio-recommendation/ — latest portfolio recommendation."""
|
|
75
|
+
return get_json("/nexus-agent/portfolio-recommendation/")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def upload_market_analysis(
|
|
79
|
+
ticker: str, content: str, language: str = "zh", metadata: Any = None
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""POST /nexus-agent/upload/market-analysis/
|
|
82
|
+
|
|
83
|
+
metadata: 结构化 metadata(emit_metadata.py 产出)。服务端存成 sidecar,
|
|
84
|
+
读端点原样返回给前端右侧面板。省略时后端不写 sidecar,前端退回 markdown 渲染。
|
|
85
|
+
"""
|
|
86
|
+
body: dict[str, Any] = {"ticker": ticker, "content": content, "language": language}
|
|
87
|
+
if metadata is not None:
|
|
88
|
+
body["metadata"] = metadata
|
|
89
|
+
return post_json("/nexus-agent/upload/market-analysis/", body)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def upload_portfolio_recommendation(
|
|
93
|
+
content: str,
|
|
94
|
+
content_cn: str | None = None,
|
|
95
|
+
time_range: str | None = None,
|
|
96
|
+
metadata: Any = None,
|
|
97
|
+
) -> Any:
|
|
98
|
+
"""POST /nexus-agent/upload/portfolio-recommendation/"""
|
|
99
|
+
body: dict[str, Any] = {"content": content}
|
|
100
|
+
if content_cn is not None:
|
|
101
|
+
body["content_cn"] = content_cn
|
|
102
|
+
if time_range is not None:
|
|
103
|
+
body["time_range"] = time_range
|
|
104
|
+
if metadata is not None:
|
|
105
|
+
body["metadata"] = metadata
|
|
106
|
+
return post_json("/nexus-agent/upload/portfolio-recommendation/", body)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def upload_backtest(ticker: str, levels: dict[str, Any]) -> Any:
|
|
110
|
+
"""POST /nexus-agent/upload/backtest/ — levels: {aggressive/conservative/normal: {...}}"""
|
|
111
|
+
return post_json("/nexus-agent/upload/backtest/", {"ticker": ticker, **levels})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_position_config(
|
|
115
|
+
ticker: str, kind: str = "normal", date_range: str | None = None
|
|
116
|
+
) -> Any:
|
|
117
|
+
"""GET /nexus-agent/position-config/ — signal_position config from S3.
|
|
118
|
+
|
|
119
|
+
Mirrors the worker's download_latest_normal_config_from_s3: the server reads
|
|
120
|
+
configs/signal_position/{ticker}/<date_range>/{ticker}-{kind}.json and returns it
|
|
121
|
+
as JSON. With date_range, fetch that specific historical version; otherwise the
|
|
122
|
+
latest. This is the Position-Research baseline the local skill cannot otherwise read.
|
|
123
|
+
"""
|
|
124
|
+
params: dict[str, Any] = {"ticker": ticker, "kind": kind}
|
|
125
|
+
if date_range:
|
|
126
|
+
params["date_range"] = date_range
|
|
127
|
+
return get_json("/nexus-agent/position-config/", params=params)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def list_history(
|
|
131
|
+
artifact_type: str, ticker: str | None = None, kind: str | None = None, since: str | None = None
|
|
132
|
+
) -> Any:
|
|
133
|
+
"""GET /nexus-agent/history/ — list historical versions of an artifact.
|
|
134
|
+
|
|
135
|
+
artifact_type: config / market-analysis / portfolio.
|
|
136
|
+
Returns {type, count, items: [{key, created_at, date_range?}, ...]} newest-first.
|
|
137
|
+
"""
|
|
138
|
+
params: dict[str, Any] = {"type": artifact_type}
|
|
139
|
+
if ticker:
|
|
140
|
+
params["ticker"] = ticker
|
|
141
|
+
if kind:
|
|
142
|
+
params["kind"] = kind
|
|
143
|
+
if since:
|
|
144
|
+
params["since"] = since
|
|
145
|
+
return get_json("/nexus-agent/history/", params=params)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def upload_position_config(
|
|
149
|
+
ticker: str, configs: dict[str, Any], time_range: str | None = None
|
|
150
|
+
) -> Any:
|
|
151
|
+
"""POST /nexus-agent/upload/position-config/ — write the 3 position configs to S3.
|
|
152
|
+
|
|
153
|
+
configs: {aggressive/conservative/normal: {<level>: {...}}}. Distinct from
|
|
154
|
+
upload_backtest (which uploads backtest aggregation, not position configs).
|
|
155
|
+
Server writes configs/signal_position/{ticker}/{time_range}/{ticker}-{kind}.json
|
|
156
|
+
and invalidates the dashboard cache.
|
|
157
|
+
"""
|
|
158
|
+
body: dict[str, Any] = {"ticker": ticker, **configs}
|
|
159
|
+
if time_range is not None:
|
|
160
|
+
body["time_range"] = time_range
|
|
161
|
+
return post_json("/nexus-agent/upload/position-config/", body)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def upload_position_report(ticker: str, content: str) -> Any:
|
|
165
|
+
"""POST /nexus-agent/upload/position-report/ — archive a position-update narrative (md)."""
|
|
166
|
+
return post_json(
|
|
167
|
+
"/nexus-agent/upload/position-report/", {"ticker": ticker, "content": content}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── discord_kb endpoints (admin only) ───────────────────────────────────────
|
|
172
|
+
#
|
|
173
|
+
# 这些端点是 Discord KB / blog draft 的**数据平面**:Discord bot token、OpenAI key、
|
|
174
|
+
# DB 凭据全部只在服务端,本地运行看不到任何密钥。
|
|
175
|
+
# 设计见 nexus-service/docs/DISCORD_KB_AND_BOT_DESIGN.md §A.6。
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def discord_clusters_pending() -> Any:
|
|
179
|
+
"""GET /discord-kb/clusters/pending/ — 取材:窗口内 pending + 所有 deferred 的簇。
|
|
180
|
+
|
|
181
|
+
⚠️ **刻意不接受 date 参数。** 取材窗口由服务端的水位线定义(上次成功运行覆盖到的
|
|
182
|
+
时刻 → 现在),不是任何自然日边界 —— 见设计文档 §B.6:cron 在 UTC 01:00 跑,而
|
|
183
|
+
北京时间晚上 11 点记的笔记属于 UTC 前一天下午,按自然日切窗口会出现某天的笔记
|
|
184
|
+
谁也没收、或者被收两次。加一个 --date 就等于把那个坑重新开出来。
|
|
185
|
+
响应里的 window 字段会告诉你这次实际覆盖了哪一段。
|
|
186
|
+
"""
|
|
187
|
+
return get_json("/discord-kb/clusters/pending/")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def discord_push_enrichment(clusters: list[dict[str, Any]]) -> Any:
|
|
191
|
+
"""POST /discord-kb/clusters/enrich/ — 回写富化结果(title/summary/tickers/topics)。
|
|
192
|
+
|
|
193
|
+
只改描述性字段,**不碰 blog_state** —— 状态迁移只走服务端的 service 层。
|
|
194
|
+
"""
|
|
195
|
+
return post_json("/discord-kb/clusters/enrich/", {"clusters": clusters})
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def discord_push_blog_draft(
|
|
199
|
+
title: str,
|
|
200
|
+
body_md: str,
|
|
201
|
+
draft_date: str,
|
|
202
|
+
source_cluster_ids: list[str],
|
|
203
|
+
front_matter: Any = None,
|
|
204
|
+
critic_report: Any = None,
|
|
205
|
+
) -> Any:
|
|
206
|
+
"""POST /discord-kb/blog-drafts/ — 提交草稿。
|
|
207
|
+
|
|
208
|
+
服务端落库并把来源簇标 `drafting`(同一事务)。`source_cluster_ids` 必须非空:
|
|
209
|
+
草稿要可追溯到来源,且标不上 drafting 的簇明天会被当 pending 重新起草。
|
|
210
|
+
⚠️ 发 #blog-review 与挂审批 reaction **尚未接线**,响应里的 discord_delivery
|
|
211
|
+
会是 "not_wired"。
|
|
212
|
+
"""
|
|
213
|
+
body: dict[str, Any] = {
|
|
214
|
+
"title": title,
|
|
215
|
+
"body_md": body_md,
|
|
216
|
+
"draft_date": draft_date,
|
|
217
|
+
"source_cluster_ids": source_cluster_ids,
|
|
218
|
+
}
|
|
219
|
+
if front_matter is not None:
|
|
220
|
+
body["front_matter"] = front_matter
|
|
221
|
+
if critic_report is not None:
|
|
222
|
+
body["critic_report"] = critic_report
|
|
223
|
+
return post_json("/discord-kb/blog-drafts/", body)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def discord_playbook_active() -> Any:
|
|
227
|
+
"""GET /discord-kb/style-playbook/active/ — 当前生效的风格 playbook。起草前必调。
|
|
228
|
+
|
|
229
|
+
⚠️ 没有 active 时服务端返回 **409**(不是 200 + null):调用方必须中止本次运行
|
|
230
|
+
并报警,绝不允许降级为无风格约束照写(§6.5)。所以这里的 RuntimeError
|
|
231
|
+
不要 catch 掉当成"没配置"。
|
|
232
|
+
"""
|
|
233
|
+
return get_json("/discord-kb/style-playbook/active/")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def discord_feedback_pending() -> Any:
|
|
237
|
+
"""GET /discord-kb/feedback/pending/ — 可折叠的审批反馈。
|
|
238
|
+
|
|
239
|
+
过滤条件全在服务端(verdict ∈ {revised, rejected} 且 note 非空 且未折叠 且指向
|
|
240
|
+
当前 active 版本),本地不要重复实现 —— approved 只进指标,expired 绝不当负信号
|
|
241
|
+
(§C.1)。响应带 should_revise 供本地判断够不够触发一次 revise。
|
|
242
|
+
"""
|
|
243
|
+
return get_json("/discord-kb/feedback/pending/")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def discord_push_playbook(
|
|
247
|
+
body_md: str,
|
|
248
|
+
rules: list[Any],
|
|
249
|
+
change_log: str = "",
|
|
250
|
+
derived_from: Any = None,
|
|
251
|
+
folded_feedback_ids: list[str] | None = None,
|
|
252
|
+
activate: bool = False,
|
|
253
|
+
) -> Any:
|
|
254
|
+
"""POST /discord-kb/style-playbook/ — 提交一版 playbook。
|
|
255
|
+
|
|
256
|
+
⚠️ **不要传 token_est** —— 服务端用 count_tokens 实算,不接受调用方上报(§C.5)。
|
|
257
|
+
超 2000 token 或 40 条规则会 422 拒收;体积变大而 change_log 的「删除」段为空
|
|
258
|
+
同样 422。
|
|
259
|
+
`activate=True` 只应用于 v1(首次从语料抽取);修订版应先落库,你在 #blog-review
|
|
260
|
+
✅ 之后再调 activate。
|
|
261
|
+
"""
|
|
262
|
+
body: dict[str, Any] = {"body_md": body_md, "rules": rules, "change_log": change_log}
|
|
263
|
+
if derived_from is not None:
|
|
264
|
+
body["derived_from"] = derived_from
|
|
265
|
+
if folded_feedback_ids:
|
|
266
|
+
body["folded_feedback_ids"] = folded_feedback_ids
|
|
267
|
+
if activate:
|
|
268
|
+
body["activate"] = True
|
|
269
|
+
return post_json("/discord-kb/style-playbook/", body)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def discord_activate_playbook(version: int) -> Any:
|
|
273
|
+
"""POST /discord-kb/style-playbook/{version}/activate/ — 相当于在 #blog-review 点 ✅。
|
|
274
|
+
|
|
275
|
+
折叠标记(folded_into)在这一步才写:若提交时就标记而最终没 ✅,那批反馈会被
|
|
276
|
+
永久消费掉、信号白丢且看不出来(§6.6)。
|
|
277
|
+
"""
|
|
278
|
+
return post_json(f"/discord-kb/style-playbook/{int(version)}/activate/", {})
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def notify(
|
|
282
|
+
to: list[str],
|
|
283
|
+
subject: str,
|
|
284
|
+
body_text: str | None = None,
|
|
285
|
+
body_html: str | None = None,
|
|
286
|
+
cc: list[str] | None = None,
|
|
287
|
+
) -> Any:
|
|
288
|
+
"""POST /nexus-agent/notify/ — send a notification email via server-side SES."""
|
|
289
|
+
body: dict[str, Any] = {"to": to, "subject": subject}
|
|
290
|
+
if cc:
|
|
291
|
+
body["cc"] = cc
|
|
292
|
+
if body_text is not None:
|
|
293
|
+
body["body_text"] = body_text
|
|
294
|
+
if body_html is not None:
|
|
295
|
+
body["body_html"] = body_html
|
|
296
|
+
return post_json("/nexus-agent/notify/", body)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def notify_report(
|
|
300
|
+
kind: str,
|
|
301
|
+
time_range: str,
|
|
302
|
+
ticker: str | None = None,
|
|
303
|
+
to: list[str] | None = None,
|
|
304
|
+
cc: list[str] | None = None,
|
|
305
|
+
expires_in: int | None = None,
|
|
306
|
+
metadata: Any = None,
|
|
307
|
+
) -> Any:
|
|
308
|
+
"""POST /nexus-agent/notify/report/ — templated weekly email with 24h presigned links.
|
|
309
|
+
|
|
310
|
+
Presigning happens server-side because the local analysis run holds no S3 credentials.
|
|
311
|
+
"""
|
|
312
|
+
body: dict[str, Any] = {"kind": kind, "time_range": time_range}
|
|
313
|
+
if ticker:
|
|
314
|
+
body["ticker"] = ticker
|
|
315
|
+
if to:
|
|
316
|
+
body["to"] = to
|
|
317
|
+
if cc:
|
|
318
|
+
body["cc"] = cc
|
|
319
|
+
if expires_in is not None:
|
|
320
|
+
body["expires_in"] = expires_in
|
|
321
|
+
if metadata is not None:
|
|
322
|
+
body["metadata"] = metadata
|
|
323
|
+
return post_json("/nexus-agent/notify/report/", body)
|
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import typer
|
|
4
4
|
|
|
5
5
|
from nexus_agent_cli.commands.auth.cmd import register as register_auth
|
|
6
|
+
from nexus_agent_cli.commands.discord.cmd import register as register_discord
|
|
6
7
|
from nexus_agent_cli.commands.run.cmd import register as register_run
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def register_all(app: typer.Typer) -> None:
|
|
10
11
|
register_auth(app)
|
|
11
12
|
register_run(app)
|
|
13
|
+
register_discord(app)
|