xg-cli 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xg/__init__.py +3 -0
- xg/__main__.py +3 -0
- xg/adaptive/__init__.py +27 -0
- xg/adaptive/calibrate.py +191 -0
- xg/adaptive/feedback.py +170 -0
- xg/adaptive/learned_rules.py +284 -0
- xg/adaptive/signals.py +125 -0
- xg/adaptive/store.py +164 -0
- xg/agent/__init__.py +0 -0
- xg/agent/plan.py +631 -0
- xg/agent/react.py +268 -0
- xg/agent/team.py +1793 -0
- xg/assets/router.lgb +0 -0
- xg/assets/router_semantics.json +21278 -0
- xg/assets/router_semantics.onnx +0 -0
- xg/cli/__init__.py +0 -0
- xg/cli/app.py +1372 -0
- xg/cli/commands.py +921 -0
- xg/cli/completion.py +740 -0
- xg/cli/help.py +185 -0
- xg/cli/train.py +200 -0
- xg/config/__init__.py +0 -0
- xg/config/env_writer.py +121 -0
- xg/config/manager.py +446 -0
- xg/config/mcp.py +229 -0
- xg/config/provider_service.py +267 -0
- xg/config/providers.py +46 -0
- xg/config/settings.py +258 -0
- xg/config/skills.py +100 -0
- xg/config/smart_router_service.py +121 -0
- xg/config/web.py +140 -0
- xg/input_history/__init__.py +7 -0
- xg/input_history/models.py +28 -0
- xg/input_history/persistence.py +126 -0
- xg/input_history/policy.py +39 -0
- xg/input_history/prompt_toolkit.py +36 -0
- xg/input_history/store.py +118 -0
- xg/llm/__init__.py +0 -0
- xg/llm/client.py +49 -0
- xg/llm/factory.py +40 -0
- xg/llm/openai_compat.py +275 -0
- xg/llm/types.py +98 -0
- xg/mcp/__init__.py +4 -0
- xg/mcp/http.py +192 -0
- xg/mcp/manager.py +726 -0
- xg/mcp/models.py +86 -0
- xg/mcp/protocol.py +62 -0
- xg/mcp/resources.py +72 -0
- xg/mcp/schema.py +137 -0
- xg/mcp/stdio.py +210 -0
- xg/mcp/transport.py +66 -0
- xg/memory/__init__.py +15 -0
- xg/memory/context.py +327 -0
- xg/memory/manager.py +111 -0
- xg/memory/models.py +41 -0
- xg/memory/project.py +187 -0
- xg/memory/store.py +144 -0
- xg/router/__init__.py +124 -0
- xg/router/features.py +66 -0
- xg/router/keywords.py +50 -0
- xg/router/ml_router.py +178 -0
- xg/router/model_tiers.py +73 -0
- xg/router/postprocess.py +167 -0
- xg/router/rule_router.py +77 -0
- xg/router/semantic.py +138 -0
- xg/safety/__init__.py +0 -0
- xg/safety/audit.py +96 -0
- xg/safety/guards.py +106 -0
- xg/safety/hitl.py +73 -0
- xg/skill/__init__.py +9 -0
- xg/skill/errors.py +45 -0
- xg/skill/loader.py +42 -0
- xg/skill/models.py +57 -0
- xg/skill/parser.py +93 -0
- xg/skill/policy.py +40 -0
- xg/skill/prompt.py +45 -0
- xg/skill/registry.py +169 -0
- xg/tool/__init__.py +0 -0
- xg/tool/builtin.py +356 -0
- xg/tool/registry.py +228 -0
- xg/tui/__init__.py +34 -0
- xg/tui/app.py +612 -0
- xg/tui/controller.py +1296 -0
- xg/tui/diagrams/__init__.py +22 -0
- xg/tui/diagrams/layout.py +110 -0
- xg/tui/diagrams/markdown.py +39 -0
- xg/tui/diagrams/model.py +36 -0
- xg/tui/diagrams/parser.py +119 -0
- xg/tui/diagrams/renderer.py +551 -0
- xg/tui/i18n.py +169 -0
- xg/tui/messages.py +45 -0
- xg/tui/plan_renderables.py +147 -0
- xg/tui/reducer.py +1029 -0
- xg/tui/renderables.py +252 -0
- xg/tui/state.py +240 -0
- xg/tui/theme.tcss +208 -0
- xg/tui/widgets/__init__.py +1 -0
- xg/tui/widgets/action_card.py +94 -0
- xg/tui/widgets/agent_group_card.py +39 -0
- xg/tui/widgets/approval_modal.py +59 -0
- xg/tui/widgets/collapsible_card.py +40 -0
- xg/tui/widgets/command_suggestions.py +128 -0
- xg/tui/widgets/composer.py +151 -0
- xg/tui/widgets/config_panel.py +198 -0
- xg/tui/widgets/confirm_modal.py +31 -0
- xg/tui/widgets/footer.py +9 -0
- xg/tui/widgets/header.py +118 -0
- xg/tui/widgets/inspector.py +378 -0
- xg/tui/widgets/plan_modal.py +53 -0
- xg/tui/widgets/provider_form.py +141 -0
- xg/tui/widgets/queue_status.py +30 -0
- xg/tui/widgets/smart_router_form.py +95 -0
- xg/tui/widgets/transcript.py +354 -0
- xg/tui/workers.py +14 -0
- xg/web/__init__.py +21 -0
- xg/web/errors.py +57 -0
- xg/web/extract.py +127 -0
- xg/web/fetch.py +106 -0
- xg/web/markdown.py +77 -0
- xg/web/models.py +91 -0
- xg/web/providers.py +79 -0
- xg/web/search.py +118 -0
- xg/web/searxng.py +27 -0
- xg/web/serpapi.py +29 -0
- xg/web/url_policy.py +110 -0
- xg/web/zhipu.py +27 -0
- xg_cli-1.0.dist-info/METADATA +284 -0
- xg_cli-1.0.dist-info/RECORD +130 -0
- xg_cli-1.0.dist-info/WHEEL +4 -0
- xg_cli-1.0.dist-info/entry_points.txt +2 -0
xg/adaptive/signals.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""隐式信号的运行时判定与采集(phase-03 步骤 B)。
|
|
2
|
+
|
|
3
|
+
判定用短关键词列表(不走 LLM),与 rule_router 特征共用同一条"中文子串匹配"
|
|
4
|
+
思路。这里只负责"识别 + 交给 FeedbackRecorder",不触碰路由/UI。
|
|
5
|
+
|
|
6
|
+
四类可采集信号:
|
|
7
|
+
- interrupt 回答中途 Ctrl+C(inline 主循环 KeyboardInterrupt 处记录)
|
|
8
|
+
- clarify 上轮后紧接追问/否定,且上轮档 ≤Superior
|
|
9
|
+
- cmd_retry 紧接重试类输入
|
|
10
|
+
- short_high_tier 简短闲聊却落到中高档(本轮档 ≥Enhanced)
|
|
11
|
+
|
|
12
|
+
红线:采集只发生在调用方显式触发时;开关关闭时调用方根本不会调用本模块,
|
|
13
|
+
因此不产生任何磁盘写。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .feedback import FeedbackRecorder, SignalType
|
|
19
|
+
|
|
20
|
+
# 简短文本判定阈值(字符数)
|
|
21
|
+
SHORT_TEXT_CHARS = 60
|
|
22
|
+
|
|
23
|
+
# 追问/否定词(clarify 触发:上轮后紧接这类输入)
|
|
24
|
+
_FOLLOWUP_WORDS = (
|
|
25
|
+
"不对", "不行", "不是这个", "再改", "再调", "重新来", "换一种", "这样呢",
|
|
26
|
+
"继续", "然后呢", "那接下来",
|
|
27
|
+
"no", "not this", "again", "instead", "rewrite", "redesign", "try again",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# 重试类词(cmd_retry 触发;与 clarify 部分重叠,但语义聚焦"重新执行")
|
|
31
|
+
_RETRY_WORDS = (
|
|
32
|
+
"重来", "再来", "重新试", "再试一次", "重做",
|
|
33
|
+
"retry", "redo", "again", "try once more",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _short_and_simple(features: dict) -> bool:
|
|
38
|
+
"""是否"简短闲聊/简单问答"(short_high_tier 的文本侧条件)。"""
|
|
39
|
+
return bool(features.get("is_chatty") or features.get("question_mark"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_clarify(text: str, prev_tier: str | None, TIER_NAMES: list[str]) -> tuple[bool, int]:
|
|
43
|
+
"""上轮后紧接追问/否定,且上轮档 ≤Superior。返回 (是否触发, 上轮档索引)。"""
|
|
44
|
+
if prev_tier is None:
|
|
45
|
+
return False, -1
|
|
46
|
+
try:
|
|
47
|
+
prev_idx = TIER_NAMES.index(prev_tier)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return False, -1
|
|
50
|
+
low = text.lower()
|
|
51
|
+
hit = any(w in low for w in _FOLLOWUP_WORDS)
|
|
52
|
+
# ≤Superior => 索引 <= 2
|
|
53
|
+
return (hit and prev_idx <= 2), prev_idx
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_cmd_retry(text: str, prev_tier: str | None) -> bool:
|
|
57
|
+
"""紧接重试类输入(需有上一轮)。"""
|
|
58
|
+
if prev_tier is None:
|
|
59
|
+
return False
|
|
60
|
+
low = text.lower()
|
|
61
|
+
return any(w in low for w in _RETRY_WORDS)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def detect_short_high_tier(text: str, features: dict, cur_tier: str | None,
|
|
65
|
+
TIER_NAMES: list[str]) -> bool:
|
|
66
|
+
"""简短闲聊却落到中高档(本轮档 ≥Enhanced,即索引 >= 1)。"""
|
|
67
|
+
if cur_tier is None:
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
cur_idx = TIER_NAMES.index(cur_tier)
|
|
71
|
+
except ValueError:
|
|
72
|
+
return False
|
|
73
|
+
if cur_idx < 1:
|
|
74
|
+
return False
|
|
75
|
+
if len(text) > SHORT_TEXT_CHARS:
|
|
76
|
+
return False
|
|
77
|
+
return _short_and_simple(features)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def capture_turn_signals(
|
|
81
|
+
recorder: FeedbackRecorder,
|
|
82
|
+
text: str,
|
|
83
|
+
features: dict,
|
|
84
|
+
prev_tier: str | None,
|
|
85
|
+
cur_tier: str,
|
|
86
|
+
TIER_NAMES: list[str],
|
|
87
|
+
) -> list[SignalType]:
|
|
88
|
+
"""对一轮普通输入判定四类信号(不含 interrupt)并采集到 recorder。
|
|
89
|
+
|
|
90
|
+
返回实际采集的信号列表,便于测试断言。clarify / cmd_retry 用上轮档,
|
|
91
|
+
short_high_tier 用本轮档(cur_tier)。
|
|
92
|
+
"""
|
|
93
|
+
emitted: list[SignalType] = []
|
|
94
|
+
|
|
95
|
+
clarify, _ = detect_clarify(text, prev_tier, TIER_NAMES)
|
|
96
|
+
if clarify:
|
|
97
|
+
recorder.capture(SignalType.CLARIFY, model_tier=prev_tier or cur_tier,
|
|
98
|
+
text=text, features=features)
|
|
99
|
+
emitted.append(SignalType.CLARIFY)
|
|
100
|
+
|
|
101
|
+
if detect_cmd_retry(text, prev_tier):
|
|
102
|
+
recorder.capture(SignalType.CMD_RETRY, model_tier=prev_tier or cur_tier,
|
|
103
|
+
text=text, features=features)
|
|
104
|
+
emitted.append(SignalType.CMD_RETRY)
|
|
105
|
+
|
|
106
|
+
if detect_short_high_tier(text, features, cur_tier, TIER_NAMES):
|
|
107
|
+
recorder.capture(SignalType.SHORT_HIGH_TIER, model_tier=cur_tier,
|
|
108
|
+
text=text, features=features)
|
|
109
|
+
emitted.append(SignalType.SHORT_HIGH_TIER)
|
|
110
|
+
|
|
111
|
+
return emitted
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def capture_interrupt(
|
|
115
|
+
recorder: FeedbackRecorder,
|
|
116
|
+
cur_tier: str | None,
|
|
117
|
+
) -> bool:
|
|
118
|
+
"""inline 主循环在 KeyboardInterrupt 处调用,记录 interrupt 信号。
|
|
119
|
+
|
|
120
|
+
需上一个已路由档位 cur_tier 作为 model_tier;无档位则跳过。
|
|
121
|
+
"""
|
|
122
|
+
if cur_tier is None:
|
|
123
|
+
return False
|
|
124
|
+
recorder.capture(SignalType.INTERRUPT, model_tier=cur_tier)
|
|
125
|
+
return True
|
xg/adaptive/store.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""adaptive 数据的持久化工具:原子写、损坏回退读取、目录懒创建。
|
|
2
|
+
|
|
3
|
+
设计依据:phase-03 步骤 A。red line 为"损坏安全"——任何读失败都回退默认
|
|
4
|
+
值,绝不抛错影响启动;写文件用 tmp + os.replace(同目录原子覆盖),
|
|
5
|
+
避免写一半导致 JSON 损坏。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
import traceback
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from . import data_dir
|
|
18
|
+
|
|
19
|
+
# 数据文件名
|
|
20
|
+
FEEDBACK_LOG = "feedback.log" # JSONL 追加,只增不改
|
|
21
|
+
CALIBRATION_JSON = "calibration.json" # 校准结果,原子写
|
|
22
|
+
LEARNED_RULES_JSON = "learned_rules.json" # 自学习规则
|
|
23
|
+
ML_ROUTER_BIN = "router.lgb" # 第 5 期 ML 精判产物(joblib 容器)
|
|
24
|
+
SEMANTIC_ONNX = "router_semantics.onnx" # 第 6 期 bge 语义编码器(ONNX int8)落盘(见 learned_rules.py)
|
|
25
|
+
SEMANTIC_ONNX_TOK = "router_semantics.json" # 与 .onnx 同名的伴生 tokenizer(semantic.py 按主文件同级取)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def feedback_log_path() -> Path:
|
|
29
|
+
return data_dir() / FEEDBACK_LOG
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def calibration_path() -> Path:
|
|
33
|
+
return data_dir() / CALIBRATION_JSON
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def learned_rules_path() -> Path:
|
|
37
|
+
return data_dir() / LEARNED_RULES_JSON
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def semantic_onnx_path() -> Path:
|
|
41
|
+
return data_dir() / SEMANTIC_ONNX
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def reset_adaptive_data() -> list[str]:
|
|
45
|
+
"""清空校准与自学习规则(feedback.log 保留作历史)。返回被删除的文件名。
|
|
46
|
+
|
|
47
|
+
phase-04 A3 的 `/smartRouter reset`。删除后校准/规则回到空态,
|
|
48
|
+
等价于"删掉 calibration.json + learned_rules.json 即回第 1 期行为"。
|
|
49
|
+
"""
|
|
50
|
+
removed: list[str] = []
|
|
51
|
+
for p in (calibration_path(), learned_rules_path()):
|
|
52
|
+
try:
|
|
53
|
+
if p.exists():
|
|
54
|
+
p.unlink()
|
|
55
|
+
removed.append(p.name)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
return removed
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ensure_dir() -> Path:
|
|
62
|
+
"""懒创建数据目录;重复调用是幂等的。"""
|
|
63
|
+
d = data_dir()
|
|
64
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
return d
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _atomic_copy(src: Path, dst: Path) -> None:
|
|
69
|
+
"""二进制原子复制:写 tmp 后 os.replace,避免写一半损坏产物。"""
|
|
70
|
+
ensure_dir()
|
|
71
|
+
fd, tmp = tempfile.mkstemp(dir=str(dst.parent), prefix=dst.name, suffix=".tmp")
|
|
72
|
+
os.close(fd)
|
|
73
|
+
try:
|
|
74
|
+
import shutil # noqa: PLC0415
|
|
75
|
+
shutil.copyfile(str(src), tmp)
|
|
76
|
+
os.replace(tmp, dst)
|
|
77
|
+
except Exception:
|
|
78
|
+
try:
|
|
79
|
+
if os.path.exists(tmp):
|
|
80
|
+
os.unlink(tmp)
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# 随包默认产物(xg/assets/,hatch wheel 打包自带):包内名称 -> 数据目录的目标路径
|
|
87
|
+
_BUNDLED_BASENAMES = (ML_ROUTER_BIN, SEMANTIC_ONNX, SEMANTIC_ONNX_TOK)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _bundled_target(name: str) -> Path:
|
|
91
|
+
"""随包产物在数据目录里的对应目标路径(与默认文件名一致)。"""
|
|
92
|
+
return data_dir() / name
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ensure_default_artifacts() -> None:
|
|
96
|
+
"""首启把随包产物复制到数据目录;目标已存在则跳过(不覆盖用户数据)。
|
|
97
|
+
|
|
98
|
+
随包资源在 ``xg/assets/``:``router.lgb``(ML 精判兜底)与
|
|
99
|
+
``router_semantics.onnx`` + ``.json``(语义编码器)。无随包资源、
|
|
100
|
+
目标已存在或复制失败时静默跳过——对应功能维持离线回落,绝不影响启动。
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
from importlib.resources import as_file, files # noqa: PLC0415
|
|
104
|
+
root = files("xg").joinpath("assets")
|
|
105
|
+
except Exception:
|
|
106
|
+
return
|
|
107
|
+
for name in _BUNDLED_BASENAMES:
|
|
108
|
+
target = _bundled_target(name)
|
|
109
|
+
if target.exists():
|
|
110
|
+
continue # 用户已有产物 → 不覆盖
|
|
111
|
+
src = root.joinpath(name)
|
|
112
|
+
if not src.is_file():
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
with as_file(src) as p_src:
|
|
116
|
+
_atomic_copy(Path(p_src), target)
|
|
117
|
+
except Exception:
|
|
118
|
+
# 内存/沙箱/只读等任何失败都静默,绝不打断启动
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _atomic_replace(path: Path, payload: str) -> None:
|
|
123
|
+
"""写 tmp 文件后 os.replace 覆盖目标,保证同目录内原子替换。"""
|
|
124
|
+
ensure_dir()
|
|
125
|
+
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=path.name, suffix=".tmp")
|
|
126
|
+
try:
|
|
127
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
128
|
+
fh.write(payload)
|
|
129
|
+
fh.flush()
|
|
130
|
+
os.fsync(fh.fileno())
|
|
131
|
+
os.replace(tmp, path)
|
|
132
|
+
except Exception:
|
|
133
|
+
# 清理残留 tmp,避免堆积
|
|
134
|
+
try:
|
|
135
|
+
if os.path.exists(tmp):
|
|
136
|
+
os.unlink(tmp)
|
|
137
|
+
except OSError:
|
|
138
|
+
pass
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def atomic_write_json(path: Path, obj: Any) -> None:
|
|
143
|
+
"""把任意 JSON 可序列化对象原子写到 ``path``。"""
|
|
144
|
+
_atomic_replace(path, json.dumps(obj, ensure_ascii=False, indent=2))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def append_jsonl(path: Path, record: dict[str, Any]) -> None:
|
|
148
|
+
"""JSONL 追加写入一行(append 模式,不覆盖既有内容)。"""
|
|
149
|
+
ensure_dir()
|
|
150
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
151
|
+
fh.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")))
|
|
152
|
+
fh.write("\n")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def read_json_safe(path: Path, default: Any = None) -> Any:
|
|
156
|
+
"""读 JSON,文件不存在 / 解析失败 / 结构异常时返回 ``default``,不抛错。"""
|
|
157
|
+
if not path.exists():
|
|
158
|
+
return default
|
|
159
|
+
try:
|
|
160
|
+
with open(path, encoding="utf-8") as fh:
|
|
161
|
+
return json.load(fh)
|
|
162
|
+
except Exception:
|
|
163
|
+
traceback.print_exc() if os.environ.get("XG_DEBUG") else None
|
|
164
|
+
return default
|
xg/agent/__init__.py
ADDED
|
File without changes
|