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.
Files changed (130) hide show
  1. xg/__init__.py +3 -0
  2. xg/__main__.py +3 -0
  3. xg/adaptive/__init__.py +27 -0
  4. xg/adaptive/calibrate.py +191 -0
  5. xg/adaptive/feedback.py +170 -0
  6. xg/adaptive/learned_rules.py +284 -0
  7. xg/adaptive/signals.py +125 -0
  8. xg/adaptive/store.py +164 -0
  9. xg/agent/__init__.py +0 -0
  10. xg/agent/plan.py +631 -0
  11. xg/agent/react.py +268 -0
  12. xg/agent/team.py +1793 -0
  13. xg/assets/router.lgb +0 -0
  14. xg/assets/router_semantics.json +21278 -0
  15. xg/assets/router_semantics.onnx +0 -0
  16. xg/cli/__init__.py +0 -0
  17. xg/cli/app.py +1372 -0
  18. xg/cli/commands.py +921 -0
  19. xg/cli/completion.py +740 -0
  20. xg/cli/help.py +185 -0
  21. xg/cli/train.py +200 -0
  22. xg/config/__init__.py +0 -0
  23. xg/config/env_writer.py +121 -0
  24. xg/config/manager.py +446 -0
  25. xg/config/mcp.py +229 -0
  26. xg/config/provider_service.py +267 -0
  27. xg/config/providers.py +46 -0
  28. xg/config/settings.py +258 -0
  29. xg/config/skills.py +100 -0
  30. xg/config/smart_router_service.py +121 -0
  31. xg/config/web.py +140 -0
  32. xg/input_history/__init__.py +7 -0
  33. xg/input_history/models.py +28 -0
  34. xg/input_history/persistence.py +126 -0
  35. xg/input_history/policy.py +39 -0
  36. xg/input_history/prompt_toolkit.py +36 -0
  37. xg/input_history/store.py +118 -0
  38. xg/llm/__init__.py +0 -0
  39. xg/llm/client.py +49 -0
  40. xg/llm/factory.py +40 -0
  41. xg/llm/openai_compat.py +275 -0
  42. xg/llm/types.py +98 -0
  43. xg/mcp/__init__.py +4 -0
  44. xg/mcp/http.py +192 -0
  45. xg/mcp/manager.py +726 -0
  46. xg/mcp/models.py +86 -0
  47. xg/mcp/protocol.py +62 -0
  48. xg/mcp/resources.py +72 -0
  49. xg/mcp/schema.py +137 -0
  50. xg/mcp/stdio.py +210 -0
  51. xg/mcp/transport.py +66 -0
  52. xg/memory/__init__.py +15 -0
  53. xg/memory/context.py +327 -0
  54. xg/memory/manager.py +111 -0
  55. xg/memory/models.py +41 -0
  56. xg/memory/project.py +187 -0
  57. xg/memory/store.py +144 -0
  58. xg/router/__init__.py +124 -0
  59. xg/router/features.py +66 -0
  60. xg/router/keywords.py +50 -0
  61. xg/router/ml_router.py +178 -0
  62. xg/router/model_tiers.py +73 -0
  63. xg/router/postprocess.py +167 -0
  64. xg/router/rule_router.py +77 -0
  65. xg/router/semantic.py +138 -0
  66. xg/safety/__init__.py +0 -0
  67. xg/safety/audit.py +96 -0
  68. xg/safety/guards.py +106 -0
  69. xg/safety/hitl.py +73 -0
  70. xg/skill/__init__.py +9 -0
  71. xg/skill/errors.py +45 -0
  72. xg/skill/loader.py +42 -0
  73. xg/skill/models.py +57 -0
  74. xg/skill/parser.py +93 -0
  75. xg/skill/policy.py +40 -0
  76. xg/skill/prompt.py +45 -0
  77. xg/skill/registry.py +169 -0
  78. xg/tool/__init__.py +0 -0
  79. xg/tool/builtin.py +356 -0
  80. xg/tool/registry.py +228 -0
  81. xg/tui/__init__.py +34 -0
  82. xg/tui/app.py +612 -0
  83. xg/tui/controller.py +1296 -0
  84. xg/tui/diagrams/__init__.py +22 -0
  85. xg/tui/diagrams/layout.py +110 -0
  86. xg/tui/diagrams/markdown.py +39 -0
  87. xg/tui/diagrams/model.py +36 -0
  88. xg/tui/diagrams/parser.py +119 -0
  89. xg/tui/diagrams/renderer.py +551 -0
  90. xg/tui/i18n.py +169 -0
  91. xg/tui/messages.py +45 -0
  92. xg/tui/plan_renderables.py +147 -0
  93. xg/tui/reducer.py +1029 -0
  94. xg/tui/renderables.py +252 -0
  95. xg/tui/state.py +240 -0
  96. xg/tui/theme.tcss +208 -0
  97. xg/tui/widgets/__init__.py +1 -0
  98. xg/tui/widgets/action_card.py +94 -0
  99. xg/tui/widgets/agent_group_card.py +39 -0
  100. xg/tui/widgets/approval_modal.py +59 -0
  101. xg/tui/widgets/collapsible_card.py +40 -0
  102. xg/tui/widgets/command_suggestions.py +128 -0
  103. xg/tui/widgets/composer.py +151 -0
  104. xg/tui/widgets/config_panel.py +198 -0
  105. xg/tui/widgets/confirm_modal.py +31 -0
  106. xg/tui/widgets/footer.py +9 -0
  107. xg/tui/widgets/header.py +118 -0
  108. xg/tui/widgets/inspector.py +378 -0
  109. xg/tui/widgets/plan_modal.py +53 -0
  110. xg/tui/widgets/provider_form.py +141 -0
  111. xg/tui/widgets/queue_status.py +30 -0
  112. xg/tui/widgets/smart_router_form.py +95 -0
  113. xg/tui/widgets/transcript.py +354 -0
  114. xg/tui/workers.py +14 -0
  115. xg/web/__init__.py +21 -0
  116. xg/web/errors.py +57 -0
  117. xg/web/extract.py +127 -0
  118. xg/web/fetch.py +106 -0
  119. xg/web/markdown.py +77 -0
  120. xg/web/models.py +91 -0
  121. xg/web/providers.py +79 -0
  122. xg/web/search.py +118 -0
  123. xg/web/searxng.py +27 -0
  124. xg/web/serpapi.py +29 -0
  125. xg/web/url_policy.py +110 -0
  126. xg/web/zhipu.py +27 -0
  127. xg_cli-1.0.dist-info/METADATA +284 -0
  128. xg_cli-1.0.dist-info/RECORD +130 -0
  129. xg_cli-1.0.dist-info/WHEEL +4 -0
  130. 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