@cnife/pi-miscs 0.1.2 → 0.2.0
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/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cnife/pi-miscs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
8
|
-
"description": "Miscellaneous small pi extensions (debug, exit)",
|
|
8
|
+
"description": "Miscellaneous small pi extensions and skills (debug, exit, measure-tokens)",
|
|
9
9
|
"homepage": "https://github.com/CNife/pi-extensions#readme",
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/CNife/pi-extensions/issues"
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"pi": {
|
|
24
24
|
"extensions": [
|
|
25
25
|
"./extensions"
|
|
26
|
+
],
|
|
27
|
+
"skills": [
|
|
28
|
+
"./skills"
|
|
26
29
|
]
|
|
27
30
|
},
|
|
28
31
|
"peerDependencies": {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: measure-tokens
|
|
3
|
+
description: 测量 pi 工具和插件的 LLM 上下文占用(token 数)
|
|
4
|
+
argument-hint: "[--analyze-only | --tokenizer <type> | --dir <path> | --help]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# measure-tokens — 测量 pi 工具的 token 开销
|
|
8
|
+
|
|
9
|
+
测量 pi 工具和插件在 LLM 上下文中的 token 占用情况,帮助优化提示词和工具定义。
|
|
10
|
+
|
|
11
|
+
## 背景
|
|
12
|
+
|
|
13
|
+
pi 工具和插件会向 LLM 发送工具定义、系统提示等文本,这些都会占用上下文窗口。了解 token 占用情况有助于:
|
|
14
|
+
|
|
15
|
+
- 识别过大的工具定义
|
|
16
|
+
- 优化提示词长度
|
|
17
|
+
- 避免上下文溢出
|
|
18
|
+
- 比较不同工具的 token 效率
|
|
19
|
+
|
|
20
|
+
## 触发方式
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 分析现有 payload 文件
|
|
24
|
+
pi run skill:measure-tokens --analyze-only
|
|
25
|
+
|
|
26
|
+
# 指定 tokenizer 类型
|
|
27
|
+
pi run skill:measure-tokens --tokenizer auto
|
|
28
|
+
|
|
29
|
+
# 捕获新的 payload(需要设置环境变量)
|
|
30
|
+
PI_DEBUG_REQUEST_BODY=1 pi [原始命令]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 使用步骤
|
|
34
|
+
|
|
35
|
+
### 1. 捕获 payload
|
|
36
|
+
|
|
37
|
+
设置环境变量 `PI_DEBUG_REQUEST_BODY=1` 运行任何 pi 命令,会将完整请求 payload 保存到临时文件:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
PI_DEBUG_REQUEST_BODY=1 pi list
|
|
41
|
+
PI_DEBUG_REQUEST_BODY=1 pi run skill:some-skill
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
每个请求会生成一个 JSON 文件,包含发送给 LLM 的完整内容。
|
|
45
|
+
|
|
46
|
+
### 2. 分析 token 占用
|
|
47
|
+
|
|
48
|
+
运行分析脚本:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 分析当前目录下的所有 payload 文件
|
|
52
|
+
uv run packages/miscs/skills/measure-tokens/measure-tokens.py --analyze-only
|
|
53
|
+
|
|
54
|
+
# 指定特定目录
|
|
55
|
+
uv run packages/miscs/skills/measure-tokens/measure-tokens.py --analyze-only --dir /path/to/payloads
|
|
56
|
+
|
|
57
|
+
# 使用特定 tokenizer(deepseek, cl100k_base, o200k_base, auto)
|
|
58
|
+
uv run packages/miscs/skills/measure-tokens/measure-tokens.py --tokenizer o200k_base
|
|
59
|
+
|
|
60
|
+
# 查看帮助
|
|
61
|
+
uv run packages/miscs/skills/measure-tokens/measure-tokens.py --help
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. 解读输出
|
|
65
|
+
|
|
66
|
+
脚本会输出:
|
|
67
|
+
|
|
68
|
+
- 每个 payload 文件的 token 统计
|
|
69
|
+
- 按工具/系统提示分类的 token 占用
|
|
70
|
+
- 总 token 数和占比
|
|
71
|
+
- 识别最大的 token 消耗者
|
|
72
|
+
|
|
73
|
+
### 4. 优化建议
|
|
74
|
+
|
|
75
|
+
根据分析结果:
|
|
76
|
+
|
|
77
|
+
- **工具定义过大**:考虑拆分工具或简化描述
|
|
78
|
+
- **系统提示过长**:精简或分段加载
|
|
79
|
+
- **重复内容**:合并相似工具或使用模板
|
|
80
|
+
- **上下文溢出**:减少同时注册的工具数量
|
|
81
|
+
|
|
82
|
+
## 输出格式
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
=== Token Analysis Report ===
|
|
86
|
+
Payload: /tmp/pi-request-20260602-100234.json
|
|
87
|
+
Total tokens: 15,432
|
|
88
|
+
|
|
89
|
+
Breakdown:
|
|
90
|
+
System prompt: 2,456 tokens (15.9%)
|
|
91
|
+
Tool definitions: 8,976 tokens (58.2%)
|
|
92
|
+
- tool1: 1,234 tokens
|
|
93
|
+
- tool2: 2,345 tokens
|
|
94
|
+
- ...
|
|
95
|
+
Messages: 4,000 tokens (25.9%)
|
|
96
|
+
|
|
97
|
+
Largest consumers:
|
|
98
|
+
1. tool3 (3,456 tokens)
|
|
99
|
+
2. tool1 (1,234 tokens)
|
|
100
|
+
3. system (2,456 tokens)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 停止条件
|
|
104
|
+
|
|
105
|
+
- 分析完成并输出报告 → 停止
|
|
106
|
+
- 无 payload 文件且 `--analyze-only` → 报告无文件并停止
|
|
107
|
+
- 用户中断 → 停止
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.12"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "deepseek-tokenizer",
|
|
6
|
+
# "tiktoken",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
测量 pi 工具的 token 开销。
|
|
12
|
+
|
|
13
|
+
用法:
|
|
14
|
+
uv run measure-tokens.py # 完整流程: 捕获 → 分析 → 报告
|
|
15
|
+
uv run measure-tokens.py --analyze-only # 只分析已有 payload
|
|
16
|
+
uv run measure-tokens.py --analyze-only --dir <path> # 从指定目录分析 payload
|
|
17
|
+
uv run measure-tokens.py --tokenizer cl100k_base # 指定分词器
|
|
18
|
+
uv run measure-tokens.py --tokenizer auto # 从 payload model 字段自动识别
|
|
19
|
+
uv run measure-tokens.py --help # 显示此帮助
|
|
20
|
+
|
|
21
|
+
支持的分词器:
|
|
22
|
+
deepseek DeepSeek 自研分词器 (vocab=128818), 对中文高效
|
|
23
|
+
cl100k_base OpenAI GPT-4 分词器 (vocab=100k)
|
|
24
|
+
o200k_base OpenAI o1 / GPT-4o 分词器 (vocab=200k)
|
|
25
|
+
auto 从 payload model 字段自动识别, 默认 deepseek
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
# ── Tokenizer selection ──────────────────────────────────────
|
|
37
|
+
_TOKENIZER_NAME = "deepseek" # default, may be overridden by --tokenizer
|
|
38
|
+
_ENCODER = None
|
|
39
|
+
|
|
40
|
+
def _load_tokenizer(name: str):
|
|
41
|
+
"""Load the specified tokenizer. Called once at startup."""
|
|
42
|
+
global _ENCODER, _TOKENIZER_NAME
|
|
43
|
+
_TOKENIZER_NAME = name
|
|
44
|
+
if name == "deepseek":
|
|
45
|
+
from deepseek_tokenizer import ds_token
|
|
46
|
+
_ENCODER = ds_token
|
|
47
|
+
elif name == "cl100k_base":
|
|
48
|
+
from tiktoken import get_encoding
|
|
49
|
+
_ENCODER = get_encoding("cl100k_base")
|
|
50
|
+
elif name == "o200k_base":
|
|
51
|
+
from tiktoken import get_encoding
|
|
52
|
+
_ENCODER = get_encoding("o200k_base")
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(f"Unknown tokenizer: {name}")
|
|
55
|
+
|
|
56
|
+
def t(s: str) -> int:
|
|
57
|
+
"""Encode and return token count."""
|
|
58
|
+
if _ENCODER is None:
|
|
59
|
+
_load_tokenizer("deepseek")
|
|
60
|
+
return len(_ENCODER.encode(s))
|
|
61
|
+
|
|
62
|
+
def detect_tokenizer_from_model(model_name: str) -> str:
|
|
63
|
+
"""Heuristic: detect which tokenizer is appropriate for a model."""
|
|
64
|
+
model_lower = model_name.lower()
|
|
65
|
+
if any(k in model_lower for k in ("deepseek", "mimo")):
|
|
66
|
+
return "deepseek"
|
|
67
|
+
elif any(k in model_lower for k in ("gpt-4", "gpt-3.5", "cl100k")):
|
|
68
|
+
return "cl100k_base"
|
|
69
|
+
return "deepseek" # fallback
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
DEBUG_DIR = Path("/tmp/pi-token-measure") # may be overridden by --dir
|
|
73
|
+
SOCKET_DIR = Path("/tmp/claude-tmux-sockets")
|
|
74
|
+
SOCKET = SOCKET_DIR / "claude.sock"
|
|
75
|
+
SESSION = "pi-measure-tokens"
|
|
76
|
+
TARGET = f"{SESSION}:1.1"
|
|
77
|
+
|
|
78
|
+
# ── CLI arg parsing ──────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
def parse_args():
|
|
81
|
+
args = {"analyze_only": False, "tokenizer": "deepseek", "dir": None}
|
|
82
|
+
i = 1
|
|
83
|
+
while i < len(sys.argv):
|
|
84
|
+
arg = sys.argv[i]
|
|
85
|
+
if arg in ("--help", "-h"):
|
|
86
|
+
print(__doc__.strip())
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
elif arg == "--analyze-only":
|
|
89
|
+
args["analyze_only"] = True
|
|
90
|
+
elif arg == "--tokenizer":
|
|
91
|
+
i += 1
|
|
92
|
+
if i < len(sys.argv):
|
|
93
|
+
args["tokenizer"] = sys.argv[i]
|
|
94
|
+
elif arg == "--dir":
|
|
95
|
+
i += 1
|
|
96
|
+
if i < len(sys.argv):
|
|
97
|
+
args["dir"] = sys.argv[i]
|
|
98
|
+
i += 1
|
|
99
|
+
return args
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── Phase 1: Capture ──────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def clean():
|
|
105
|
+
if DEBUG_DIR.exists():
|
|
106
|
+
shutil.rmtree(DEBUG_DIR)
|
|
107
|
+
DEBUG_DIR.mkdir(parents=True)
|
|
108
|
+
SOCKET_DIR.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
def start_pi():
|
|
111
|
+
subprocess.run(["tmux", "-S", str(SOCKET), "kill-session", "-t", SESSION], capture_output=True)
|
|
112
|
+
time.sleep(0.3)
|
|
113
|
+
subprocess.run(["tmux", "-S", str(SOCKET), "new", "-d", "-s", SESSION], capture_output=True)
|
|
114
|
+
time.sleep(0.3)
|
|
115
|
+
subprocess.run(["tmux", "-S", str(SOCKET), "send-keys", "-t", TARGET, "--",
|
|
116
|
+
f"export PI_DEBUG_REQUEST_BODY={DEBUG_DIR} && pi"], capture_output=True)
|
|
117
|
+
print(f" pi started, debug at {DEBUG_DIR}")
|
|
118
|
+
|
|
119
|
+
def send(text: str):
|
|
120
|
+
subprocess.run(["tmux", "-S", str(SOCKET), "send-keys", "-t", TARGET, "--", text], capture_output=True)
|
|
121
|
+
|
|
122
|
+
def capture(lines: int = 50) -> str:
|
|
123
|
+
r = subprocess.run(["tmux", "-S", str(SOCKET), "capture-pane", "-p", "-J", "-t", TARGET,
|
|
124
|
+
"-S", f"-{lines}"], capture_output=True, text=True)
|
|
125
|
+
return r.stdout
|
|
126
|
+
|
|
127
|
+
def wait_output(pattern: str, timeout: int = 60) -> bool:
|
|
128
|
+
import re
|
|
129
|
+
deadline = time.time() + timeout
|
|
130
|
+
while time.time() < deadline:
|
|
131
|
+
if re.search(pattern, capture(100), re.IGNORECASE):
|
|
132
|
+
return True
|
|
133
|
+
time.sleep(1)
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── Phase 2: Analyze ──────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def analyze() -> dict:
|
|
140
|
+
files = sorted(DEBUG_DIR.glob("*.json"))
|
|
141
|
+
if not files:
|
|
142
|
+
return {"from_payload": False}
|
|
143
|
+
|
|
144
|
+
print(f"\n Found {len(files)} payload files")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
with open(files[0]) as f:
|
|
148
|
+
base = json.load(f)
|
|
149
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
150
|
+
print(f" ⚠ Failed to load payload: {e}")
|
|
151
|
+
return {"from_payload": False}
|
|
152
|
+
|
|
153
|
+
sys_msg = base["messages"][0]["content"] if base.get("messages") else ""
|
|
154
|
+
tools_list = base.get("tools", [])
|
|
155
|
+
model_name = base.get("model", "")
|
|
156
|
+
|
|
157
|
+
# Tool definitions from payload
|
|
158
|
+
ask_tool = todo_tool = None
|
|
159
|
+
for te in tools_list:
|
|
160
|
+
fn = te.get("function", te)
|
|
161
|
+
n = fn.get("name", "")
|
|
162
|
+
if n == "ask_user_question":
|
|
163
|
+
ask_tool = json.dumps(te, ensure_ascii=False)
|
|
164
|
+
elif n == "todo":
|
|
165
|
+
todo_tool = json.dumps(te, ensure_ascii=False)
|
|
166
|
+
|
|
167
|
+
ask_args = ask_result = ""
|
|
168
|
+
todo_calls = []
|
|
169
|
+
for fp in files:
|
|
170
|
+
try:
|
|
171
|
+
with open(fp) as f:
|
|
172
|
+
data = json.load(f)
|
|
173
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
174
|
+
print(f" ⚠ Skipping corrupted payload {fp.name}: {e}")
|
|
175
|
+
continue
|
|
176
|
+
for msg in data.get("messages", []):
|
|
177
|
+
for tc in msg.get("tool_calls", []):
|
|
178
|
+
fn = tc.get("function", {})
|
|
179
|
+
n = fn.get("name", "")
|
|
180
|
+
if n == "ask_user_question" and not ask_args:
|
|
181
|
+
ask_args = fn.get("arguments", "")
|
|
182
|
+
elif n == "todo" and fn.get("arguments", "") not in todo_calls:
|
|
183
|
+
todo_calls.append(fn.get("arguments", ""))
|
|
184
|
+
if msg["role"] == "tool":
|
|
185
|
+
ct = msg.get("content", "")
|
|
186
|
+
if "User has answered your questions" in ct:
|
|
187
|
+
ask_result = ct
|
|
188
|
+
|
|
189
|
+
ask_g = todo_g = ""
|
|
190
|
+
if "Use ask_user_question whenever" in sys_msg:
|
|
191
|
+
s = sys_msg.find("Use ask_user_question whenever")
|
|
192
|
+
e = sys_msg.find("\n- Use `todo`")
|
|
193
|
+
if e > s: ask_g = sys_msg[s:e].strip()
|
|
194
|
+
if "Use `todo` for complex" in sys_msg:
|
|
195
|
+
s = sys_msg.find("Use `todo` for complex")
|
|
196
|
+
e = sys_msg.find("\n- Be concise in your")
|
|
197
|
+
if e > s: todo_g = sys_msg[s:e].strip()
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"ask_tool": ask_tool, "todo_tool": todo_tool,
|
|
201
|
+
"ask_args": ask_args, "ask_result": ask_result,
|
|
202
|
+
"ask_guide": ask_g, "todo_guide": todo_g,
|
|
203
|
+
"todo_calls": todo_calls,
|
|
204
|
+
"model_name": model_name,
|
|
205
|
+
"from_payload": True,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def compute(data: dict) -> dict:
|
|
210
|
+
# ── All-tools static from payload ──
|
|
211
|
+
all_tools = []
|
|
212
|
+
sys_tok = 0
|
|
213
|
+
if data["from_payload"]:
|
|
214
|
+
files = sorted(DEBUG_DIR.glob("*.json"))
|
|
215
|
+
try:
|
|
216
|
+
with open(files[0]) as f:
|
|
217
|
+
base = json.load(f)
|
|
218
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
219
|
+
print(f" ⚠ Failed to reload payload: {e}")
|
|
220
|
+
base = {}
|
|
221
|
+
sys_content = base["messages"][0]["content"]
|
|
222
|
+
sys_tok = t(sys_content)
|
|
223
|
+
for te in base.get("tools", []):
|
|
224
|
+
fn = te.get("function", te)
|
|
225
|
+
full_tok = t(json.dumps(te, ensure_ascii=False))
|
|
226
|
+
desc_tok = t(fn.get("description", ""))
|
|
227
|
+
params_tok = t(json.dumps(fn.get("parameters", {}), ensure_ascii=False))
|
|
228
|
+
all_tools.append({"name": fn.get("name", "?"), "full": full_tok,
|
|
229
|
+
"desc": desc_tok, "params": params_tok})
|
|
230
|
+
all_tools.sort(key=lambda x: x["full"], reverse=True)
|
|
231
|
+
|
|
232
|
+
total_all_tools = sum(x["full"] for x in all_tools)
|
|
233
|
+
|
|
234
|
+
if data["from_payload"] and data.get("ask_tool") and data.get("todo_tool"):
|
|
235
|
+
ask_tok = t(data["ask_tool"])
|
|
236
|
+
todo_tok = t(data["todo_tool"])
|
|
237
|
+
else:
|
|
238
|
+
ask_tok = 910
|
|
239
|
+
todo_tok = 505
|
|
240
|
+
|
|
241
|
+
ask_g_tok = t(data["ask_guide"]) if data.get("ask_guide") else 0
|
|
242
|
+
todo_g_tok = t(data["todo_guide"]) if data.get("todo_guide") else 0
|
|
243
|
+
|
|
244
|
+
ask_ol = t("ask_user_question: Ask the user up to 4 structured questions (2-4 options each) when requirements are ambiguous")
|
|
245
|
+
todo_ol = t("todo: Manage a task list to track multi-step progress")
|
|
246
|
+
|
|
247
|
+
aa_tok = t(data["ask_args"]) if data.get("ask_args") else 0
|
|
248
|
+
ar_tok = t(data["ask_result"]) if data.get("ask_result") else 0
|
|
249
|
+
todo_toks = [t(c) for c in data.get("todo_calls", [])]
|
|
250
|
+
|
|
251
|
+
static_rpiv = ask_tok + todo_tok + ask_g_tok + todo_g_tok + ask_ol + todo_ol
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"tokenizer": _TOKENIZER_NAME,
|
|
255
|
+
"all_tools": all_tools,
|
|
256
|
+
"total_all_tools": total_all_tools,
|
|
257
|
+
"sys_tok": sys_tok,
|
|
258
|
+
"vocab": getattr(_ENCODER, 'vocab_size', None) or getattr(_ENCODER, 'max_token_value', None) or 100000,
|
|
259
|
+
"ask_tok": ask_tok,
|
|
260
|
+
"todo_tok": todo_tok,
|
|
261
|
+
"ask_g_tok": ask_g_tok,
|
|
262
|
+
"todo_g_tok": todo_g_tok,
|
|
263
|
+
"ask_ol": ask_ol,
|
|
264
|
+
"todo_ol": todo_ol,
|
|
265
|
+
"ask_args_tok": aa_tok,
|
|
266
|
+
"ask_result_tok": ar_tok,
|
|
267
|
+
"todo_toks": todo_toks,
|
|
268
|
+
"static_rpiv": static_rpiv,
|
|
269
|
+
"payload_count": len(list(DEBUG_DIR.glob("*.json"))) if data["from_payload"] else 0,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def report(r: dict):
|
|
274
|
+
avg_c = sum(r["todo_toks"]) // len(r["todo_toks"]) if r["todo_toks"] else 0
|
|
275
|
+
ask_static = r["ask_tok"] + r["ask_g_tok"] + r["ask_ol"]
|
|
276
|
+
todo_static = r["todo_tok"] + r["todo_g_tok"] + r["todo_ol"]
|
|
277
|
+
ask_dyn = r["ask_args_tok"] + r["ask_result_tok"]
|
|
278
|
+
|
|
279
|
+
print()
|
|
280
|
+
print("=" * 62)
|
|
281
|
+
tk_name = r["tokenizer"]
|
|
282
|
+
tk_vocab = f", vocab={r['vocab']}" if r['vocab'] else ""
|
|
283
|
+
print(f" Token Overhead Report ({tk_name}{tk_vocab})")
|
|
284
|
+
src = f"{r['payload_count']} payloads" if r["payload_count"] else "fallback strings"
|
|
285
|
+
print(f" Source: {src}")
|
|
286
|
+
print("=" * 62)
|
|
287
|
+
|
|
288
|
+
# ── All tools ranking ──
|
|
289
|
+
if r["all_tools"]:
|
|
290
|
+
print()
|
|
291
|
+
print(" ┌─ All Tools Static Overhead (tools[] JSON) ────────────┐")
|
|
292
|
+
print(f" │ {'Tool':<28s} {'Full':>6s} {'Desc':>6s} {'Params':>6s} │")
|
|
293
|
+
print(f" │ {'─'*28} {'─'*6} {'─'*6} {'─'*6} │")
|
|
294
|
+
for ts in r["all_tools"]:
|
|
295
|
+
tag = " ← rpiv" if ts["name"] in ("ask_user_question", "todo") else ""
|
|
296
|
+
print(f" │ {ts['name']:<28s} {ts['full']:>6d} {ts['desc']:>6d} {ts['params']:>6d}{tag}")
|
|
297
|
+
print(f" │ {'─'*28} {'─'*6} {'─'*6} {'─'*6} │")
|
|
298
|
+
print(f" │ {'Total':<28s} {r['total_all_tools']:>6d} │")
|
|
299
|
+
print(f" └──────────────────────────────────────────────────────┘")
|
|
300
|
+
if r["sys_tok"]:
|
|
301
|
+
non_tool = r["sys_tok"] - r["total_all_tools"]
|
|
302
|
+
print(f" System prompt: {r['sys_tok']:>6d} tokens total")
|
|
303
|
+
print(f" ├─ tools[] definitions: {r['total_all_tools']:>6d} ({r['total_all_tools']/r['sys_tok']*100:.1f}%)")
|
|
304
|
+
print(f" │ └─ rpiv tools: {r['ask_tok']+r['todo_tok']:>6d} ({(r['ask_tok']+r['todo_tok'])/r['sys_tok']*100:.1f}%)")
|
|
305
|
+
print(f" └─ other (instructions, {non_tool:>6d} ({non_tool/r['sys_tok']*100:.1f}%)")
|
|
306
|
+
print(f" skills, context, etc.)")
|
|
307
|
+
|
|
308
|
+
print()
|
|
309
|
+
print(" ┌─ rpiv Tools Focus ────────────────────────────────────┐")
|
|
310
|
+
print(f" │ ask_user_question tool JSON: {r['ask_tok']:>5d} tokens │")
|
|
311
|
+
print(f" │ todo tool JSON: {r['todo_tok']:>5d} tokens │")
|
|
312
|
+
print(f" │ ask guidelines (4 rules): {r['ask_g_tok']:>5d} tokens │")
|
|
313
|
+
print(f" │ todo guidelines (7 rules): {r['todo_g_tok']:>5d} tokens │")
|
|
314
|
+
print(f" │ one-liner refs (ask+todo): {r['ask_ol']+r['todo_ol']:>5d} tokens │")
|
|
315
|
+
print(f" ├──────────────────────────────────────────────────────┤")
|
|
316
|
+
print(f" │ Subtotal (rpiv static): {r['static_rpiv']:>5d} tokens │")
|
|
317
|
+
print(f" └──────────────────────────────────────────────────────┘")
|
|
318
|
+
print()
|
|
319
|
+
print(" ┌─ Dynamic Overhead ────────────────────────────────────┐")
|
|
320
|
+
print(f" │ ask_user_question (4 questions, ~3 options each): │")
|
|
321
|
+
print(f" │ call arguments: {r['ask_args_tok']:>5d} tokens │")
|
|
322
|
+
print(f" │ tool result: {r['ask_result_tok']:>5d} tokens │")
|
|
323
|
+
print(f" │ subtotal: ~{ask_dyn:>5d} tokens │")
|
|
324
|
+
print(f" ├──────────────────────────────────────────────────────┤")
|
|
325
|
+
print(f" │ todo: {len(r['todo_toks'])} creates │")
|
|
326
|
+
for i, tok in enumerate(r["todo_toks"]):
|
|
327
|
+
print(f" │ #{i+1}: {tok:>5d} tokens │")
|
|
328
|
+
print(f" │ avg: ~{avg_c:>5d} tokens/call │")
|
|
329
|
+
print(f" └──────────────────────────────────────────────────────┘")
|
|
330
|
+
print()
|
|
331
|
+
print(" ┌─ Decision Guide (per round) ─────────────────────────┐")
|
|
332
|
+
ROUNDING_BUFFER = 100 # unmeasured tool call overhead buffer
|
|
333
|
+
both = ask_static + ask_dyn + todo_static + avg_c * 3 + ROUNDING_BUFFER
|
|
334
|
+
print(f" │ ask_user_question only: {ask_static+ask_dyn:>5d} tokens/round │")
|
|
335
|
+
print(f" │ todo only (3 ops): {todo_static+avg_c*3+100:>5d} tokens/round │")
|
|
336
|
+
print(f" │ both tools used: {both:>5d} tokens/round │")
|
|
337
|
+
print(f" │ rpiv static only: {r['static_rpiv']:>5d} tokens/round │")
|
|
338
|
+
print(f" │ ALL tools static (13 tools): {r['total_all_tools']:>5d} tokens/round │")
|
|
339
|
+
print(f" ├──────────────────────────────────────────────────────┤")
|
|
340
|
+
print(f" │ 128K context: ~{128000//both:>3d} rounds (both tools) │")
|
|
341
|
+
print(f" └──────────────────────────────────────────────────────┘")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ── CLI ────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
def main():
|
|
347
|
+
args = parse_args()
|
|
348
|
+
|
|
349
|
+
# Apply --dir override
|
|
350
|
+
global DEBUG_DIR
|
|
351
|
+
if args["dir"]:
|
|
352
|
+
DEBUG_DIR = Path(args["dir"])
|
|
353
|
+
|
|
354
|
+
# Initialize tokenizer
|
|
355
|
+
if args["tokenizer"] == "auto":
|
|
356
|
+
if not args["analyze_only"]:
|
|
357
|
+
print(" ⚠ --tokenizer auto only applies in --analyze-only mode; using deepseek for capture")
|
|
358
|
+
# Defer to after payload analysis for model detection
|
|
359
|
+
else:
|
|
360
|
+
_load_tokenizer(args["tokenizer"])
|
|
361
|
+
|
|
362
|
+
if args["analyze_only"]:
|
|
363
|
+
print(f"Tokenizer: {args['tokenizer']}")
|
|
364
|
+
data = analyze()
|
|
365
|
+
if args["tokenizer"] == "auto" and data.get("model_name"):
|
|
366
|
+
detected = detect_tokenizer_from_model(data["model_name"])
|
|
367
|
+
print(f" Auto-detected from model '{data['model_name']}': {detected}")
|
|
368
|
+
_load_tokenizer(detected)
|
|
369
|
+
elif args["tokenizer"] == "auto":
|
|
370
|
+
_load_tokenizer("deepseek")
|
|
371
|
+
r = compute(data)
|
|
372
|
+
report(r)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
print("Phase 1/3: Capturing data")
|
|
376
|
+
clean()
|
|
377
|
+
start_pi()
|
|
378
|
+
if not wait_output(r"╰─", timeout=45):
|
|
379
|
+
print(" ⚠ pi may not have started fully")
|
|
380
|
+
# Continue anyway — best-effort capture
|
|
381
|
+
time.sleep(3)
|
|
382
|
+
|
|
383
|
+
send("帮我设计一个笔记方案。每天写技术笔记,需要快速搜索、标签、导出PDF。纠结用 Notion 还是本地文件。先问清楚需求再给建议。")
|
|
384
|
+
if wait_output(r"ask_user_question", timeout=45):
|
|
385
|
+
print(" ✓ ask_user_question triggered, answering...")
|
|
386
|
+
time.sleep(2)
|
|
387
|
+
# Answer questions dynamically — send Enter for each question
|
|
388
|
+
import re
|
|
389
|
+
for q in range(4):
|
|
390
|
+
pane_before = capture(50)
|
|
391
|
+
send("Enter")
|
|
392
|
+
time.sleep(1.5)
|
|
393
|
+
pane_after = capture(50)
|
|
394
|
+
if q < 3:
|
|
395
|
+
send("Tab")
|
|
396
|
+
time.sleep(0.5)
|
|
397
|
+
# If pane content didn't change, assume no more questions
|
|
398
|
+
if pane_before == pane_after:
|
|
399
|
+
break
|
|
400
|
+
send("Enter")
|
|
401
|
+
time.sleep(4)
|
|
402
|
+
print(" ✓ questionnaire answered")
|
|
403
|
+
else:
|
|
404
|
+
print(" ⚠ ask_user_question not triggered")
|
|
405
|
+
|
|
406
|
+
if wait_output(r"需要我帮你|推荐|方案", timeout=60):
|
|
407
|
+
print(" Response received, triggering todo...")
|
|
408
|
+
time.sleep(2)
|
|
409
|
+
send("做一个研究项目:对比分析 Docker、Podman、containerd。分三个阶段:收集资料、对比分析、写总结报告。请用 todos 管理任务进度。")
|
|
410
|
+
if wait_output(r"todo|Todos|Created #", timeout=45):
|
|
411
|
+
print(" ✓ todo triggered")
|
|
412
|
+
else:
|
|
413
|
+
print(" ⚠ todo not triggered")
|
|
414
|
+
|
|
415
|
+
time.sleep(3)
|
|
416
|
+
send("C-d")
|
|
417
|
+
time.sleep(2)
|
|
418
|
+
subprocess.run(["tmux", "-S", str(SOCKET), "kill-session", "-t", SESSION], capture_output=True)
|
|
419
|
+
print(" Data capture complete.\n")
|
|
420
|
+
|
|
421
|
+
print("Phase 2/3: Analyzing")
|
|
422
|
+
data = analyze()
|
|
423
|
+
print("\nPhase 3/3: Report")
|
|
424
|
+
r = compute(data)
|
|
425
|
+
report(r)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
if __name__ == "__main__":
|
|
429
|
+
main()
|