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,22 @@
1
+ """Dependency-free Mermaid flowchart support for the TUI."""
2
+
3
+ from xg.tui.diagrams.markdown import MermaidBlock, split_mermaid_blocks
4
+ from xg.tui.diagrams.layout import FlowchartLayout, LayoutNode, layout_flowchart
5
+ from xg.tui.diagrams.model import FlowchartEdge, FlowchartModel, FlowchartNode
6
+ from xg.tui.diagrams.parser import FlowchartParseError, parse_flowchart
7
+ from xg.tui.diagrams.renderer import DiagramRender, render_flowchart
8
+
9
+ __all__ = [
10
+ "FlowchartEdge",
11
+ "FlowchartLayout",
12
+ "FlowchartModel",
13
+ "FlowchartNode",
14
+ "DiagramRender",
15
+ "FlowchartParseError",
16
+ "LayoutNode",
17
+ "MermaidBlock",
18
+ "parse_flowchart",
19
+ "layout_flowchart",
20
+ "render_flowchart",
21
+ "split_mermaid_blocks",
22
+ ]
@@ -0,0 +1,110 @@
1
+ """Stable, bounded layout for the supported Mermaid flowchart model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from xg.tui.diagrams.model import FlowchartModel
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class LayoutNode:
12
+ """A node position in a rank/row layout."""
13
+
14
+ node_id: str
15
+ rank: int
16
+ order: int
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class FlowchartLayout:
21
+ """Deterministic placement metadata used by the text renderer."""
22
+
23
+ nodes: tuple[LayoutNode, ...]
24
+ ranks: tuple[tuple[str, ...], ...]
25
+ cyclic: bool = False
26
+
27
+ def position(self, node_id: str) -> LayoutNode:
28
+ for node in self.nodes:
29
+ if node.node_id == node_id:
30
+ return node
31
+ raise KeyError(node_id)
32
+
33
+
34
+ def layout_flowchart(
35
+ model: FlowchartModel,
36
+ *,
37
+ max_iterations: int | None = None,
38
+ rank_by_node: dict[str, int] | None = None,
39
+ ) -> FlowchartLayout:
40
+ """Assign nodes to stable layers without an unbounded graph traversal.
41
+
42
+ DAGs use the longest known predecessor rank. For cyclic graphs the same
43
+ relaxation is capped at ``node_count`` iterations and the remaining nodes
44
+ keep their stable source order. This is deliberately small and
45
+ predictable rather than a full Mermaid-compatible layout engine.
46
+ """
47
+ node_ids = [node.id for node in model.nodes]
48
+ index = {node_id: i for i, node_id in enumerate(node_ids)}
49
+ incoming: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
50
+ for edge in model.edges:
51
+ if edge.source in incoming and edge.target in incoming:
52
+ incoming[edge.target].append(edge.source)
53
+
54
+ limit = max_iterations if max_iterations is not None else max(1, len(node_ids))
55
+ ranks = {node_id: 0 for node_id in node_ids}
56
+ if rank_by_node is not None:
57
+ # Callers such as Plan already have a business-level batch partition.
58
+ # Honor it while retaining a safe default for omitted nodes.
59
+ ranks.update({node_id: rank for node_id, rank in rank_by_node.items() if node_id in ranks and rank >= 0})
60
+ grouped: dict[int, list[str]] = {}
61
+ for node_id in node_ids:
62
+ grouped.setdefault(ranks[node_id], []).append(node_id)
63
+ ordered_ranks = sorted(grouped)
64
+ if model.direction in ("BT", "RL"):
65
+ ordered_ranks.reverse()
66
+ rank_lists = [tuple(grouped[rank]) for rank in ordered_ranks]
67
+ positions = tuple(
68
+ LayoutNode(node_id=node_id, rank=rank_no, order=order)
69
+ for rank_no, rank_nodes in enumerate(rank_lists)
70
+ for order, node_id in enumerate(rank_nodes)
71
+ )
72
+ return FlowchartLayout(nodes=positions, ranks=tuple(rank_lists), cyclic=False)
73
+
74
+ changed = False
75
+ for _ in range(limit):
76
+ changed = False
77
+ for node_id in node_ids:
78
+ predecessors = incoming[node_id]
79
+ if not predecessors:
80
+ continue
81
+ candidate = max(ranks[pred] + 1 for pred in predecessors)
82
+ if candidate > ranks[node_id]:
83
+ ranks[node_id] = candidate
84
+ changed = True
85
+ if not changed:
86
+ break
87
+
88
+ cyclic = changed
89
+ # A cycle can make the relaxation increase every rank on every pass. Keep
90
+ # it bounded and collapse the inflated ranks to a useful stable layout.
91
+ if cyclic:
92
+ ranks = {node_id: 0 for node_id in node_ids}
93
+ for node_id in node_ids:
94
+ predecessors = [p for p in incoming[node_id] if index[p] < index[node_id]]
95
+ ranks[node_id] = min(1, max((ranks[p] + 1 for p in predecessors), default=0))
96
+
97
+ grouped: dict[int, list[str]] = {}
98
+ for node_id in node_ids:
99
+ grouped.setdefault(ranks[node_id], []).append(node_id)
100
+ ordered_ranks = sorted(grouped)
101
+ if model.direction in ("BT", "RL"):
102
+ ordered_ranks.reverse()
103
+
104
+ rank_lists = [tuple(grouped[rank]) for rank in ordered_ranks]
105
+ positions = tuple(
106
+ LayoutNode(node_id=node_id, rank=rank_no, order=order)
107
+ for rank_no, rank_nodes in enumerate(rank_lists)
108
+ for order, node_id in enumerate(rank_nodes)
109
+ )
110
+ return FlowchartLayout(nodes=positions, ranks=tuple(rank_lists), cyclic=cyclic)
@@ -0,0 +1,39 @@
1
+ """Recognize complete Mermaid fenced blocks without parsing Markdown globally."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class MermaidBlock:
11
+ source: str
12
+ start: int
13
+ end: int
14
+
15
+
16
+ _FENCE_RE = re.compile(
17
+ r"```[ \t]*mermaid[ \t]*\r?\n(?P<body>.*?)(?P<fence>```)[ \t]*(?:\r?\n|$)",
18
+ re.IGNORECASE | re.DOTALL,
19
+ )
20
+
21
+
22
+ def split_mermaid_blocks(text: str) -> list[tuple[str, MermaidBlock | None]]:
23
+ """Split text into ``("text", None)`` and complete Mermaid blocks.
24
+
25
+ An unclosed fence is deliberately returned as ordinary text. This keeps
26
+ streaming assistant output visible as source until the fence is complete.
27
+ """
28
+ parts: list[tuple[str, MermaidBlock | None]] = []
29
+ cursor = 0
30
+ for match in _FENCE_RE.finditer(text):
31
+ if match.start() > cursor:
32
+ parts.append((text[cursor : match.start()], None))
33
+ parts.append((match.group("body"), MermaidBlock(
34
+ source=match.group("body"), start=match.start(), end=match.end()
35
+ )))
36
+ cursor = match.end()
37
+ if cursor < len(text):
38
+ parts.append((text[cursor:], None))
39
+ return parts or [(text, None)]
@@ -0,0 +1,36 @@
1
+ """Pure data structures for the supported Mermaid flowchart subset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Literal
7
+
8
+
9
+ Direction = Literal["TD", "TB", "BT", "LR", "RL"]
10
+ NodeShape = Literal["rect", "round", "diamond", "circle"]
11
+ EdgeStyle = Literal["solid", "dotted", "thick"]
12
+
13
+
14
+ @dataclass
15
+ class FlowchartNode:
16
+ id: str
17
+ label: str
18
+ shape: NodeShape = "rect"
19
+
20
+
21
+ @dataclass
22
+ class FlowchartEdge:
23
+ source: str
24
+ target: str
25
+ label: str = ""
26
+ style: EdgeStyle = "solid"
27
+ arrow: bool = True
28
+
29
+
30
+ @dataclass
31
+ class FlowchartModel:
32
+ direction: Direction = "TD"
33
+ nodes: list[FlowchartNode] = field(default_factory=list)
34
+ edges: list[FlowchartEdge] = field(default_factory=list)
35
+ source: str = ""
36
+ warnings: list[str] = field(default_factory=list)
@@ -0,0 +1,119 @@
1
+ """Small, safe parser for the common Mermaid flowchart syntax."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+
8
+ from xg.tui.diagrams.model import FlowchartEdge, FlowchartModel, FlowchartNode
9
+
10
+
11
+ class FlowchartParseError(ValueError):
12
+ """The source is not a supported flowchart subset."""
13
+
14
+
15
+ _HEADER_RE = re.compile(r"^(?:flowchart|graph)(?:\s+(TD|TB|BT|LR|RL))?\s*$", re.IGNORECASE)
16
+ _NODE_RE = re.compile(
17
+ r"\s*(?P<id>[A-Za-z_][\w-]*)"
18
+ r"(?P<body>\(\([^\n()]*\)\)|\[[^\n\]]*\]|\([^\n()]*\)|\{[^\n}]*\}|>[^\n\]]*\])?"
19
+ )
20
+ _PIPE_LABEL_RE = re.compile(r"\|(?P<label>[^|]*)\|")
21
+ _EDGE_OPS = ("-.->", "==>", "-->", "---")
22
+
23
+
24
+ def _clean_label(value: str) -> str:
25
+ value = html.unescape(value.replace("<br/>", "\n").replace("<br>", "\n"))
26
+ value = re.sub(r"<[^>]+>", "", value)
27
+ return value.strip()
28
+
29
+
30
+ def _node_parts(node_id: str, body: str | None) -> tuple[str, str]:
31
+ if not body:
32
+ return node_id, "rect"
33
+ if body.startswith("(("):
34
+ return _clean_label(body[2:-2]) or node_id, "circle"
35
+ if body.startswith("["):
36
+ return _clean_label(body[1:-1]) or node_id, "rect"
37
+ if body.startswith("("):
38
+ return _clean_label(body[1:-1]) or node_id, "round"
39
+ if body.startswith("{"):
40
+ return _clean_label(body[1:-1]) or node_id, "diamond"
41
+ if body.startswith(">"): # Mermaid flag shape: >label]
42
+ return _clean_label(body[1:-1]) or node_id, "rect"
43
+ return node_id, "rect"
44
+
45
+
46
+ def parse_flowchart(source: str, *, max_nodes: int = 80, max_edges: int = 160, max_label_chars: int = 120) -> FlowchartModel:
47
+ """Parse a bounded subset of ``flowchart`` / ``graph`` Mermaid syntax."""
48
+ lines = [line.strip() for line in source.splitlines() if line.strip() and not line.lstrip().startswith("%%")]
49
+ if not lines:
50
+ raise FlowchartParseError("flowchart 内容为空")
51
+ header = _HEADER_RE.fullmatch(lines[0])
52
+ if not header:
53
+ raise FlowchartParseError("只支持 flowchart/graph 语法及 TD/TB/BT/LR/RL 方向")
54
+ direction = (header.group(1) or "TD").upper()
55
+ model = FlowchartModel(direction=direction, source=source)
56
+ nodes: dict[str, FlowchartNode] = {}
57
+
58
+ def ensure_node(node_id: str, body: str | None) -> None:
59
+ label, shape = _node_parts(node_id, body)
60
+ if len(label) > max_label_chars:
61
+ label = label[:max_label_chars] + "…"
62
+ model.warnings.append(f"节点 {node_id} 标签已截断")
63
+ existing = nodes.get(node_id)
64
+ if existing is None:
65
+ if len(nodes) >= max_nodes:
66
+ raise FlowchartParseError(f"节点数超过上限 {max_nodes}")
67
+ nodes[node_id] = FlowchartNode(id=node_id, label=label, shape=shape) # type: ignore[arg-type]
68
+ elif body:
69
+ existing.label = label
70
+ existing.shape = shape # type: ignore[assignment]
71
+
72
+ def parse_node(text: str, pos: int) -> tuple[str, str | None, int]:
73
+ match = _NODE_RE.match(text, pos)
74
+ if not match:
75
+ raise FlowchartParseError(f"无法解析节点:{text[pos:].strip()[:80]}")
76
+ return match.group("id"), match.group("body"), match.end()
77
+
78
+ for line_no, line in enumerate(lines[1:], 2):
79
+ if line.lower().startswith("subgraph") or line.lower().startswith("end"):
80
+ raise FlowchartParseError(f"第 {line_no} 行不支持 subgraph")
81
+ for statement in (part.strip() for part in line.split(";") if part.strip()):
82
+ pos = 0
83
+ source_id, source_body, pos = parse_node(statement, pos)
84
+ ensure_node(source_id, source_body)
85
+ while pos < len(statement):
86
+ rest = statement[pos:].lstrip()
87
+ pos = len(statement) - len(rest)
88
+ label = ""
89
+ style = "solid"
90
+ op = next((candidate for candidate in _EDGE_OPS if rest.startswith(candidate)), None)
91
+ if op is not None:
92
+ pos += len(op)
93
+ style = "dotted" if op == "-.->" else "thick" if op == "==>" else "solid"
94
+ label_match = _PIPE_LABEL_RE.match(statement, pos)
95
+ if label_match:
96
+ label = _clean_label(label_match.group("label"))
97
+ pos = label_match.end()
98
+ elif rest.startswith("--"):
99
+ # Support the readable form: A -- label --> B.
100
+ end = rest.find("-->", 2)
101
+ if end < 0:
102
+ raise FlowchartParseError(f"第 {line_no} 行的连线不完整")
103
+ label = _clean_label(rest[2:end])
104
+ pos += end + 3
105
+ else:
106
+ raise FlowchartParseError(f"第 {line_no} 行存在不支持的语法:{rest[:80]}")
107
+ target_id, target_body, pos = parse_node(statement, pos)
108
+ ensure_node(target_id, target_body)
109
+ if len(model.edges) >= max_edges:
110
+ raise FlowchartParseError(f"连线数超过上限 {max_edges}")
111
+ model.edges.append(FlowchartEdge(
112
+ source=source_id, target=target_id, label=label,
113
+ style=style, arrow=True, # type: ignore[arg-type]
114
+ ))
115
+ source_id = target_id
116
+ model.nodes = list(nodes.values())
117
+ if not model.nodes:
118
+ raise FlowchartParseError("没有找到节点")
119
+ return model