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
@@ -0,0 +1,198 @@
1
+ """统一配置面板(TUI,Ctrl+T):Provider 与 SmartRouter 同级展示。
2
+
3
+ 仅于 TUI 运行时 import。Provider 列表与 SmartRouter 四档配置在同一个
4
+ Modal 内左右分栏平级呈现;编辑/新建仍走各自的 Form Modal,开关状态变更
5
+ 即时同步顶部 Header。动作落 :class:`provider_service.ProviderConfigService`
6
+ 与 :class:`smart_router_service.SmartRouterConfigService`。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from textual.app import ComposeResult
12
+ from textual.containers import Horizontal, Vertical
13
+ from textual.screen import ModalScreen
14
+ from textual.widgets import Button, OptionList, Static
15
+ from textual.widgets.option_list import Option
16
+
17
+ from xg.config.provider_service import ProviderConfigService
18
+ from xg.config.smart_router_service import SmartRouterConfigService
19
+ from xg.tui.widgets.provider_form import ProviderForm
20
+ from xg.tui.widgets.smart_router_form import SmartRouterForm
21
+
22
+
23
+ class ConfigScreen(ModalScreen[None]):
24
+ BINDINGS = [("escape", "close", "关闭")]
25
+
26
+ def __init__(self, manager, settings=None) -> None:
27
+ super().__init__()
28
+ self.manager = manager
29
+ self.settings = settings
30
+ self.provider_service = ProviderConfigService(manager, settings)
31
+ self.router_service = SmartRouterConfigService(manager, settings)
32
+ self._rows: list[dict] = []
33
+ self._sel = 0
34
+
35
+ def compose(self) -> ComposeResult:
36
+ with Vertical(id="config-dialog"):
37
+ yield Static("配置中心", id="cfg-title")
38
+ with Vertical(id="cfg-body"):
39
+ with Vertical(id="provider-section"):
40
+ yield Static("Provider 管理(或 /provider 命令)", id="cfg-sec-title")
41
+ yield Static("↑↓ 选中行 · 操作作用于当前选中", id="provider-hint")
42
+ yield OptionList(id="provider-list")
43
+ yield Static("", id="provider-detail")
44
+ with Horizontal(id="provider-actions"):
45
+ yield Button("新增", id="add", variant="primary")
46
+ yield Button("编辑", id="edit")
47
+ yield Button("设为base", id="switch")
48
+ yield Button("删除", id="remove", variant="error")
49
+ yield Button("写Key", id="key")
50
+ with Vertical(id="sr-section"):
51
+ yield Static("SmartRouter(或 /tier 命令)", id="cfg-sec-title")
52
+ yield Static(self._render_router(), id="sr-body")
53
+ with Horizontal(id="sr-actions"):
54
+ yield Button("开关", id="toggle")
55
+ yield Button("编辑档位", id="tier", variant="primary")
56
+ yield Button("刷新", id="refresh")
57
+ yield Button("关闭", id="close")
58
+
59
+ def _provider_label(self, row: dict) -> str:
60
+ return (
61
+ f"{row['name']:<12}"
62
+ f"{row['default_model'][:16]:<16}"
63
+ f"{'✓' if row['has_key'] else '✗':^5}"
64
+ f"{'●' if row['is_base'] else ' ':^6}"
65
+ f"{row['layer']}"
66
+ )
67
+
68
+ def _rebuild_provider_list(self, ol: OptionList) -> None:
69
+ self._rows = self.provider_service.list()
70
+ ol.clear_options()
71
+ if not self._rows:
72
+ self._sel = 0
73
+ return
74
+ for row in self._rows:
75
+ ol.add_option(Option(self._provider_label(row), id=row["name"]))
76
+ if self._sel >= len(self._rows):
77
+ self._sel = 0
78
+ ol.highlighted = self._sel
79
+
80
+ async def on_mount(self) -> None:
81
+ self._rebuild_provider_list(self.query_one("#provider-list", OptionList))
82
+ self.query_one("#sr-body", Static).update(self._render_router())
83
+ self._update_detail()
84
+
85
+ def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None:
86
+ self._sel = event.option_index
87
+ self._update_detail()
88
+
89
+ def _update_detail(self) -> None:
90
+ """展示当前选中 provider 的详情(等价 /provider show,key 脱敏)。"""
91
+ if not self._rows:
92
+ detail = "(无 provider,请新增)"
93
+ else:
94
+ name = self._rows[self._sel]["name"]
95
+ info = self.provider_service.get(name)
96
+ if info is None:
97
+ detail = f"{name}: 未知 provider"
98
+ else:
99
+ lines = [
100
+ f"{info['name']}{' ●base' if info['is_base'] else ''} [{info['layer']} 层]",
101
+ f"api_base : {info['api_base']}",
102
+ f"default_model: {info['default_model']}",
103
+ ]
104
+ if info.get("display_name"):
105
+ lines.insert(1, f"display_name : {info['display_name']}")
106
+ models = info.get("models") or []
107
+ lines.append(f"models({len(models)}) : {', '.join(models) if models else '(空)'}")
108
+ lines.append(
109
+ f"api_key : {info['api_key_masked'] if info['has_key'] else '(未配置)'}"
110
+ )
111
+ refs = self.provider_service.referenced_by(name)
112
+ if refs:
113
+ lines.append(f"SmartRouter 引用: {', '.join(refs)}")
114
+ detail = "\n".join(lines)
115
+ self.query_one("#provider-detail", Static).update(detail)
116
+
117
+ def _render_router(self) -> str:
118
+ cfg = self.router_service.get()
119
+ enabled = bool(cfg.get("enabled", False))
120
+ rows = self.router_service.list_tiers()
121
+ lines = [f"SmartRouter: {'开启' if enabled else '关闭'}",
122
+ "TIER PROVIDER MODEL"]
123
+ for row in rows:
124
+ if row["configured"]:
125
+ lines.append(f"{row['name']:<10}{row['provider']:<14}{row['model']}")
126
+ else:
127
+ lines.append(f"{row['name']:<10}{'(回落 active)':<14}-")
128
+ return "\n".join(lines)
129
+
130
+ def _refresh(self) -> None:
131
+ self._rebuild_provider_list(self.query_one("#provider-list", OptionList))
132
+ self.query_one("#sr-body", Static).update(self._render_router())
133
+ self._update_detail()
134
+
135
+ def _sync_header(self) -> None:
136
+ """开关 / 档位变更后即时重建顶部 Header 路由行。"""
137
+ controller = getattr(self.app, "controller", None)
138
+ if controller is not None:
139
+ controller._sync_smart_router_snapshot()
140
+
141
+ def action_close(self) -> None:
142
+ self.dismiss(None)
143
+
144
+ def on_button_pressed(self, event: Button.Pressed) -> None:
145
+ bid = event.button.id or ""
146
+ if bid == "close":
147
+ self.dismiss(None)
148
+ return
149
+ if bid == "refresh":
150
+ self._refresh()
151
+ return
152
+ if bid == "add":
153
+ return self.app.push_screen(
154
+ ProviderForm(self.provider_service, mode="add"),
155
+ callback=self._after_form,
156
+ )
157
+ if bid == "toggle":
158
+ cfg = self.router_service.get()
159
+ on = not bool(cfg.get("enabled", False))
160
+ self.notify(self.router_service.set_enabled(on).message)
161
+ self._refresh()
162
+ self._sync_header()
163
+ return
164
+ if bid == "tier":
165
+ return self.app.push_screen(
166
+ SmartRouterForm(self.router_service), callback=self._after_form
167
+ )
168
+ if not self._rows:
169
+ self.notify("请先新增 provider", severity="warning")
170
+ return
171
+ if bid == "edit":
172
+ return self.app.push_screen(
173
+ ProviderForm(self.provider_service, name=self._rows[self._sel]["name"], mode="edit"),
174
+ callback=self._after_form,
175
+ )
176
+ if bid == "switch":
177
+ self.notify(self.provider_service.switch(self._rows[self._sel]["name"]).message)
178
+ self._refresh()
179
+ return
180
+ if bid == "remove":
181
+ result = self.provider_service.remove(self._rows[self._sel]["name"], yes=True)
182
+ self.notify(result.message, severity="error" if not result.ok else "information")
183
+ self._refresh()
184
+ return
185
+ if bid == "key":
186
+ return self.app.push_screen(
187
+ ProviderForm(self.provider_service, name=self._rows[self._sel]["name"], mode="key"),
188
+ callback=self._after_form,
189
+ )
190
+
191
+ def _after_form(self, _result: object) -> None:
192
+ self._refresh()
193
+ self._sync_header()
194
+
195
+
196
+ def open_config_screen(app, manager, settings=None) -> None:
197
+ """外部调用:打开统一配置面板(Ctrl+T)。"""
198
+ app.push_screen(ConfigScreen(manager, settings))
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Vertical
5
+ from textual.screen import ModalScreen
6
+ from textual.widgets import Button, Static
7
+
8
+ from xg.tui.state import ConfirmationRequest
9
+
10
+
11
+ class ConfirmModal(ModalScreen[str]):
12
+ BINDINGS = [("escape", "cancel", "取消"), ("enter", "confirm", "确认"), ("y", "confirm", "确认")]
13
+
14
+ def __init__(self, request: ConfirmationRequest) -> None:
15
+ super().__init__()
16
+ self.request = request
17
+
18
+ def compose(self) -> ComposeResult:
19
+ with Vertical(id="confirm-dialog"):
20
+ yield Static(f"{self.request.title}\n\n{self.request.body}")
21
+ yield Button("确认 (y/Enter)", id="confirm", variant="warning")
22
+ yield Button("取消 (Esc)", id="cancel")
23
+
24
+ def action_confirm(self) -> None:
25
+ self.dismiss("confirm")
26
+
27
+ def action_cancel(self) -> None:
28
+ self.dismiss("cancel")
29
+
30
+ def on_button_pressed(self, event: Button.Pressed) -> None:
31
+ self.dismiss(event.button.id or "cancel")
@@ -0,0 +1,9 @@
1
+ from textual.widgets import Static
2
+
3
+
4
+ class FooterBar(Static):
5
+ def __init__(self) -> None:
6
+ super().__init__(
7
+ "Enter 发送 /help 帮助 ↑/↓ 历史 Ctrl+C 取消 Ctrl+L 清屏 Ctrl+R 侧栏 Ctrl+T 配置 Esc 关闭弹窗",
8
+ id="footer",
9
+ )
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from rich.text import Text
6
+ from textual.widgets import Static
7
+
8
+ from xg.tui.state import SmartRouterSnapshot, SmartRouterTierSnapshot, TuiState
9
+
10
+
11
+ def _compact(value: int) -> str:
12
+ if value <= 0:
13
+ return "-"
14
+ if value < 1_000:
15
+ return str(value)
16
+ if value < 1_000_000:
17
+ return f"{value / 1_000:.1f}k".replace(".0k", "k")
18
+ return f"{value / 1_000_000:.1f}m".replace(".0m", "m")
19
+
20
+
21
+ def _watermelon_mark() -> str:
22
+ """Return the brand mark with a safe ASCII fallback for dumb terminals."""
23
+ return "[XG CLI]" if os.environ.get("TERM", "").lower() == "dumb" else "🍉 XG CLI"
24
+
25
+
26
+ def _status_label(phase: str) -> str:
27
+ return {
28
+ "idle": "Idle",
29
+ "running": "Working",
30
+ "awaiting_approval": "Waiting approval",
31
+ "awaiting_plan_review": "Plan review",
32
+ "awaiting_team_input": "Waiting team input",
33
+ "error": "Error",
34
+ }.get(phase, phase)
35
+
36
+
37
+ def _tier_segment(tier: SmartRouterTierSnapshot) -> Text:
38
+ """Render one tier entry on the routing row.
39
+
40
+ 当前档 bold reverse;未配置档 dim + ``model (x)``;其余档 dim。
41
+ """
42
+ label = f"{tier.tier}: {tier.model}"
43
+ if not tier.configured:
44
+ label += " (x)"
45
+ if tier.is_active:
46
+ return Text(label, style="bold reverse")
47
+ return Text(label, style="dim")
48
+
49
+
50
+ def _routing_row(snapshot: SmartRouterSnapshot) -> Text:
51
+ """Assemble the four-tier routing row (only when enabled)."""
52
+ row = Text()
53
+ for index, tier in enumerate(snapshot.tiers):
54
+ if index:
55
+ row.append(" ")
56
+ row.append(_tier_segment(tier))
57
+ return row
58
+
59
+
60
+ class HeaderBar(Static):
61
+ def __init__(self, *args, **kwargs) -> None:
62
+ # The ASCII fallback is rendered literally; otherwise Textual markup
63
+ # would treat the square brackets in ``[XG]`` as a tag.
64
+ kwargs.setdefault("markup", False)
65
+ super().__init__(*args, **kwargs)
66
+
67
+ def update_state(self, state: TuiState) -> None:
68
+ status = _status_label(state.phase)
69
+ inspector = state.inspector
70
+ usage = inspector.usage
71
+ level = "usage-normal"
72
+ highest_ratio = max(usage.window_ratio, usage.budget_usage_ratio)
73
+ if highest_ratio > 1.0:
74
+ level = "usage-error"
75
+ elif highest_ratio >= 0.7:
76
+ level = "usage-warning"
77
+ for class_name in ("usage-normal", "usage-warning", "usage-error"):
78
+ self.remove_class(class_name)
79
+ self.add_class(level)
80
+ available = usage.estimated_prompt_tokens > 0 and usage.context_window > 0
81
+ context = (
82
+ f"{_compact(usage.estimated_prompt_tokens)}/{_compact(usage.context_window)}"
83
+ if available else "-/-"
84
+ )
85
+ percentage = f"{usage.window_ratio * 100:.1f}%" if available else "-"
86
+ provider_model = f"{inspector.provider}/{inspector.model}".strip("/") or "provider/model unavailable"
87
+ hitl_enabled = inspector.safety.hitl_enabled if inspector.safety else inspector.hitl_enabled
88
+ queue = f" · Queue {len(state.queue)}" if state.queue else ""
89
+
90
+ # Textual terminals cannot reliably display an SVG/PNG in the normal
91
+ # text pipeline. The watermelon emoji is the color-capable mark, with
92
+ # [XG] retained for TERM=dumb and other low-capability environments.
93
+ mark = _watermelon_mark()
94
+ # This is intentionally a welcome/brand block rather than a one-line
95
+ # status bar. Keeping the runtime fields in the same widget preserves
96
+ # the existing state-update contract while giving the left column a
97
+ # useful visual hierarchy.
98
+ text = Text()
99
+ text.append(mark, style="bold bright_green")
100
+ text.append("\n")
101
+ text.append(provider_model, style="bold")
102
+ text.append("\n")
103
+ # SmartRouter routing row (phase-02): rendered only when enabled so the
104
+ # off-state output stays byte-identical to the pre-feature Header.
105
+ if inspector.smart_router.enabled and inspector.smart_router.tiers:
106
+ text.append(_routing_row(inspector.smart_router))
107
+ text.append("\n")
108
+ status_style = "red" if state.phase == "error" else "yellow" if state.phase != "idle" else "dim"
109
+ text.append(status, style=status_style)
110
+ text.append(" · ")
111
+ text.append(f"Context {context} · {percentage}")
112
+ text.append("\n")
113
+ text.append("HITL ON" if hitl_enabled else "HITL OFF", style="yellow" if not hitl_enabled else "dim")
114
+ text.append(queue, style="cyan")
115
+ # Static.update(Rich Text) needs an active Textual app console in
116
+ # Textual 8.x. Keep standalone render tests and lightweight adapters
117
+ # usable by falling back to the plain representation when unmounted.
118
+ self.update(text if self.is_mounted else text.plain)