vox-code 2.0.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.
- vox_code-2.0.0.dist-info/METADATA +258 -0
- vox_code-2.0.0.dist-info/RECORD +88 -0
- vox_code-2.0.0.dist-info/WHEEL +4 -0
- vox_code-2.0.0.dist-info/entry_points.txt +3 -0
- voxcli/__init__.py +3 -0
- voxcli/__main__.py +5 -0
- voxcli/agent/__init__.py +12 -0
- voxcli/agent/agent.py +449 -0
- voxcli/agent/agent_budget.py +133 -0
- voxcli/agent/agent_orchestrator.py +414 -0
- voxcli/agent/plan_execute_agent.py +514 -0
- voxcli/agent/roles.py +80 -0
- voxcli/agent/sub_agent.py +351 -0
- voxcli/catalog.py +477 -0
- voxcli/chat.py +91 -0
- voxcli/cli/__init__.py +4 -0
- voxcli/cli/main.py +452 -0
- voxcli/cli/parser.py +71 -0
- voxcli/config.py +518 -0
- voxcli/gui/__main__.py +3 -0
- voxcli/gui/main.py +22 -0
- voxcli/gui/pet/__init__.py +5 -0
- voxcli/gui/pet/base.py +62 -0
- voxcli/gui/pet/coordinator.py +888 -0
- voxcli/gui/pet/data.py +430 -0
- voxcli/gui/pet/widgets.py +683 -0
- voxcli/gui/pet/windows.py +2298 -0
- voxcli/gui/pet/workers.py +54 -0
- voxcli/gui/pet_app.py +7 -0
- voxcli/hitl/__init__.py +11 -0
- voxcli/hitl/handler.py +11 -0
- voxcli/hitl/policy.py +32 -0
- voxcli/hitl/request.py +13 -0
- voxcli/hitl/result.py +11 -0
- voxcli/hitl/terminal_handler.py +64 -0
- voxcli/hitl/tool_registry.py +64 -0
- voxcli/llm/base.py +93 -0
- voxcli/llm/factory.py +178 -0
- voxcli/llm/ollama_client.py +137 -0
- voxcli/llm/openai_compatible.py +249 -0
- voxcli/memory/base.py +16 -0
- voxcli/memory/budget.py +53 -0
- voxcli/memory/compressor.py +198 -0
- voxcli/memory/entry.py +36 -0
- voxcli/memory/long_term.py +126 -0
- voxcli/memory/manager.py +101 -0
- voxcli/memory/retriever.py +72 -0
- voxcli/memory/short_term.py +84 -0
- voxcli/memory/tokenizer.py +21 -0
- voxcli/plan/__init__.py +5 -0
- voxcli/plan/execution_plan.py +225 -0
- voxcli/plan/planner.py +198 -0
- voxcli/plan/task.py +123 -0
- voxcli/policy/audit_log.py +111 -0
- voxcli/policy/command_guard.py +34 -0
- voxcli/policy/exception.py +5 -0
- voxcli/policy/path_guard.py +32 -0
- voxcli/prompting/__init__.py +7 -0
- voxcli/prompting/presenter.py +154 -0
- voxcli/rag/__init__.py +16 -0
- voxcli/rag/analyzer.py +89 -0
- voxcli/rag/chunk.py +17 -0
- voxcli/rag/chunker.py +137 -0
- voxcli/rag/embedding.py +75 -0
- voxcli/rag/formatter.py +40 -0
- voxcli/rag/index.py +96 -0
- voxcli/rag/relation.py +14 -0
- voxcli/rag/retriever.py +58 -0
- voxcli/rag/store.py +155 -0
- voxcli/rag/tokenizer.py +26 -0
- voxcli/runtime/__init__.py +6 -0
- voxcli/runtime/session_controller.py +386 -0
- voxcli/tool/__init__.py +3 -0
- voxcli/tool/tool_registry.py +433 -0
- voxcli/util/animation.py +219 -0
- voxcli/util/ansi.py +82 -0
- voxcli/util/markdown.py +98 -0
- voxcli/web/__init__.py +17 -0
- voxcli/web/base.py +20 -0
- voxcli/web/extractor.py +77 -0
- voxcli/web/factory.py +38 -0
- voxcli/web/fetch_result.py +27 -0
- voxcli/web/fetcher.py +42 -0
- voxcli/web/network_policy.py +49 -0
- voxcli/web/result.py +23 -0
- voxcli/web/searxng.py +55 -0
- voxcli/web/serpapi.py +53 -0
- voxcli/web/zhipu.py +55 -0
voxcli/memory/manager.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Memory 管理器 - Memory 系统的门面类"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from ..llm.base import LlmClient
|
|
7
|
+
from .entry import MemoryEntry, MemoryType, estimate_tokens
|
|
8
|
+
from .short_term import ConversationMemory
|
|
9
|
+
from .long_term import LongTermMemory
|
|
10
|
+
from .retriever import MemoryRetriever
|
|
11
|
+
from .budget import TokenBudget
|
|
12
|
+
from .compressor import ContextCompressor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MAX_TOOL_RESULT_CHARS = 500
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MemoryManager:
|
|
19
|
+
def __init__(self, llm_client: LlmClient,
|
|
20
|
+
short_term_budget: int = 32768,
|
|
21
|
+
context_window: int = 200000,
|
|
22
|
+
long_term: Optional[LongTermMemory] = None):
|
|
23
|
+
self._short_term = ConversationMemory(short_term_budget)
|
|
24
|
+
self._long_term = long_term or LongTermMemory()
|
|
25
|
+
self._compressor = ContextCompressor(llm_client)
|
|
26
|
+
self._retriever = MemoryRetriever(self._short_term, self._long_term)
|
|
27
|
+
self._budget = TokenBudget(context_window)
|
|
28
|
+
self._llm = llm_client
|
|
29
|
+
|
|
30
|
+
def set_llm_client(self, llm_client: LlmClient):
|
|
31
|
+
self._llm = llm_client
|
|
32
|
+
self._compressor.set_llm_client(llm_client)
|
|
33
|
+
|
|
34
|
+
def add_user_message(self, content: str):
|
|
35
|
+
entry = MemoryEntry(
|
|
36
|
+
id=f"user-{uuid.uuid4().hex[:8]}",
|
|
37
|
+
content=content,
|
|
38
|
+
type=MemoryType.CONVERSATION,
|
|
39
|
+
metadata={"source": "user"},
|
|
40
|
+
)
|
|
41
|
+
self._short_term.store(entry)
|
|
42
|
+
self._compress_if_needed()
|
|
43
|
+
|
|
44
|
+
def add_assistant_message(self, content: str):
|
|
45
|
+
entry = MemoryEntry(
|
|
46
|
+
id=f"assistant-{uuid.uuid4().hex[:8]}",
|
|
47
|
+
content=content,
|
|
48
|
+
type=MemoryType.CONVERSATION,
|
|
49
|
+
metadata={"source": "assistant"},
|
|
50
|
+
)
|
|
51
|
+
self._short_term.store(entry)
|
|
52
|
+
self._compress_if_needed()
|
|
53
|
+
|
|
54
|
+
def add_tool_result(self, tool_name: str, result: str):
|
|
55
|
+
truncated = result[:_MAX_TOOL_RESULT_CHARS] + "...(已截断)" if len(result) > _MAX_TOOL_RESULT_CHARS else result
|
|
56
|
+
content = f"[{tool_name}] {truncated}"
|
|
57
|
+
entry = MemoryEntry(
|
|
58
|
+
id=f"tool-{uuid.uuid4().hex[:8]}",
|
|
59
|
+
content=content,
|
|
60
|
+
type=MemoryType.TOOL_RESULT,
|
|
61
|
+
metadata={"source": "tool", "toolName": tool_name},
|
|
62
|
+
)
|
|
63
|
+
self._short_term.store(entry)
|
|
64
|
+
self._compress_if_needed()
|
|
65
|
+
|
|
66
|
+
def store_fact(self, fact: str):
|
|
67
|
+
entry = MemoryEntry(
|
|
68
|
+
id=f"fact-{uuid.uuid4().hex[:8]}",
|
|
69
|
+
content=fact,
|
|
70
|
+
type=MemoryType.FACT,
|
|
71
|
+
metadata={"source": "fact"},
|
|
72
|
+
)
|
|
73
|
+
self._long_term.store(entry)
|
|
74
|
+
|
|
75
|
+
def retrieve_relevant(self, query: str, limit: int) -> List[MemoryEntry]:
|
|
76
|
+
return self._retriever.retrieve(query, limit)
|
|
77
|
+
|
|
78
|
+
def build_context_for_query(self, query: str, max_tokens: int) -> str:
|
|
79
|
+
return self._retriever.build_context_for_query(query, max_tokens)
|
|
80
|
+
|
|
81
|
+
def record_token_usage(self, input_tokens: int, output_tokens: int):
|
|
82
|
+
self._budget.record_usage(input_tokens, output_tokens)
|
|
83
|
+
|
|
84
|
+
def clear_short_term(self):
|
|
85
|
+
self._short_term.clear()
|
|
86
|
+
|
|
87
|
+
def clear_long_term(self):
|
|
88
|
+
self._long_term.clear()
|
|
89
|
+
|
|
90
|
+
def status_summary(self) -> str:
|
|
91
|
+
return (f"{self._short_term.status_summary()}\n"
|
|
92
|
+
f"{self._long_term.status_summary()}\n"
|
|
93
|
+
f"{self._budget.usage_report}")
|
|
94
|
+
|
|
95
|
+
def _compress_if_needed(self):
|
|
96
|
+
if not self._budget.needs_compression(self._short_term):
|
|
97
|
+
return
|
|
98
|
+
print("📦 短期记忆接近预算上限,触发压缩...")
|
|
99
|
+
summary = self._compressor.compress(self._short_term)
|
|
100
|
+
if summary:
|
|
101
|
+
print(f" 压缩完成,摘要: {summary[:100]}...")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""记忆检索器"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from .entry import MemoryEntry, MemoryType
|
|
7
|
+
from .long_term import LongTermMemory
|
|
8
|
+
from .short_term import ConversationMemory
|
|
9
|
+
from .tokenizer import tokenize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MemoryRetriever:
|
|
13
|
+
def __init__(self, short_term: ConversationMemory, long_term: LongTermMemory):
|
|
14
|
+
self._short_term = short_term
|
|
15
|
+
self._long_term = long_term
|
|
16
|
+
|
|
17
|
+
def retrieve(self, query: str, limit: int) -> List[MemoryEntry]:
|
|
18
|
+
scored: list[tuple[MemoryEntry, float]] = []
|
|
19
|
+
for entry in self._short_term.get_all():
|
|
20
|
+
score = self._compute_relevance(entry, query)
|
|
21
|
+
if score > 0:
|
|
22
|
+
scored.append((entry, score))
|
|
23
|
+
for entry in self._long_term.get_all():
|
|
24
|
+
score = self._compute_relevance(entry, query) * 1.2
|
|
25
|
+
if score > 0:
|
|
26
|
+
scored.append((entry, score))
|
|
27
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
28
|
+
return [e for e, _ in scored[:limit]]
|
|
29
|
+
|
|
30
|
+
def retrieve_long_term(self, query: str, limit: int) -> List[MemoryEntry]:
|
|
31
|
+
scored: list[tuple[MemoryEntry, float]] = []
|
|
32
|
+
for entry in self._long_term.get_all():
|
|
33
|
+
score = self._compute_relevance(entry, query) * 1.2
|
|
34
|
+
if score > 0:
|
|
35
|
+
scored.append((entry, score))
|
|
36
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
37
|
+
return [e for e, _ in scored[:limit]]
|
|
38
|
+
|
|
39
|
+
def build_context_for_query(self, query: str, max_tokens: int) -> str:
|
|
40
|
+
relevant = self.retrieve_long_term(query, 10)
|
|
41
|
+
if not relevant:
|
|
42
|
+
return ""
|
|
43
|
+
parts = ["## 相关长期记忆\n"]
|
|
44
|
+
used = 0
|
|
45
|
+
for entry in relevant:
|
|
46
|
+
if used + entry.token_count > max_tokens:
|
|
47
|
+
break
|
|
48
|
+
parts.append(f"- [{entry.type.value}] {entry.content}\n")
|
|
49
|
+
used += entry.token_count
|
|
50
|
+
parts.append("\n")
|
|
51
|
+
return "".join(parts)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _compute_relevance(entry: MemoryEntry, query: str) -> float:
|
|
55
|
+
content_lower = entry.content.lower()
|
|
56
|
+
query_lower = query.lower()
|
|
57
|
+
|
|
58
|
+
if query_lower in content_lower:
|
|
59
|
+
return 1.0
|
|
60
|
+
|
|
61
|
+
query_words = tokenize(query_lower)
|
|
62
|
+
if not query_words:
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
65
|
+
matched = sum(1 for w in query_words if w in content_lower)
|
|
66
|
+
if matched == 0:
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
keyword_score = matched / len(query_words)
|
|
70
|
+
age_hours = (time.time() - entry.timestamp) / 3600
|
|
71
|
+
time_decay = max(0.5, 1.0 - age_hours / 24.0)
|
|
72
|
+
return keyword_score * time_decay
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""短期记忆 - 管理当前对话上下文"""
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from .base import Memory
|
|
7
|
+
from .entry import MemoryEntry
|
|
8
|
+
from .tokenizer import matches, tokenize
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConversationMemory(Memory):
|
|
12
|
+
def __init__(self, max_tokens: int = 32768):
|
|
13
|
+
self._entries: OrderedDict[str, MemoryEntry] = OrderedDict()
|
|
14
|
+
self._max_tokens = max_tokens
|
|
15
|
+
self._current_tokens = 0
|
|
16
|
+
self._compressed_summaries: List[MemoryEntry] = []
|
|
17
|
+
|
|
18
|
+
def store(self, entry: MemoryEntry):
|
|
19
|
+
self._entries[entry.id] = entry
|
|
20
|
+
self._current_tokens += entry.token_count
|
|
21
|
+
while self._current_tokens > self._max_tokens and len(self._entries) > 1:
|
|
22
|
+
self._evict_oldest()
|
|
23
|
+
|
|
24
|
+
def retrieve(self, id: str) -> Optional[MemoryEntry]:
|
|
25
|
+
return self._entries.get(id)
|
|
26
|
+
|
|
27
|
+
def search(self, query: str, limit: int) -> List[MemoryEntry]:
|
|
28
|
+
query_tokens = tokenize(query)
|
|
29
|
+
results = []
|
|
30
|
+
for entry in self._entries.values():
|
|
31
|
+
if matches(entry.content, query_tokens):
|
|
32
|
+
results.append(entry)
|
|
33
|
+
if len(results) >= limit:
|
|
34
|
+
break
|
|
35
|
+
return results
|
|
36
|
+
|
|
37
|
+
def get_all(self) -> List[MemoryEntry]:
|
|
38
|
+
return list(self._entries.values())
|
|
39
|
+
|
|
40
|
+
def delete(self, id: str) -> bool:
|
|
41
|
+
entry = self._entries.pop(id, None)
|
|
42
|
+
if entry:
|
|
43
|
+
self._current_tokens -= entry.token_count
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def clear(self):
|
|
48
|
+
self._entries.clear()
|
|
49
|
+
self._current_tokens = 0
|
|
50
|
+
self._compressed_summaries.clear()
|
|
51
|
+
|
|
52
|
+
def token_count(self) -> int:
|
|
53
|
+
return self._current_tokens
|
|
54
|
+
|
|
55
|
+
def size(self) -> int:
|
|
56
|
+
return len(self._entries)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def max_tokens(self) -> int:
|
|
60
|
+
return self._max_tokens
|
|
61
|
+
|
|
62
|
+
def get_compressed_summaries(self) -> List[MemoryEntry]:
|
|
63
|
+
return list(self._compressed_summaries)
|
|
64
|
+
|
|
65
|
+
def inject_summary(self, summary: MemoryEntry):
|
|
66
|
+
self._compressed_summaries.clear()
|
|
67
|
+
self._entries[summary.id] = summary
|
|
68
|
+
self._current_tokens += summary.token_count
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def usage_ratio(self) -> float:
|
|
72
|
+
return self._current_tokens / self._max_tokens if self._max_tokens > 0 else 0.0
|
|
73
|
+
|
|
74
|
+
def status_summary(self) -> str:
|
|
75
|
+
return (f"短期记忆: {self.size()}条 / {self._current_tokens} tokens "
|
|
76
|
+
f"(预算: {self._max_tokens}, 使用率: {self.usage_ratio * 100:.0f}%, "
|
|
77
|
+
f"已压缩: {len(self._compressed_summaries)}条)")
|
|
78
|
+
|
|
79
|
+
def _evict_oldest(self):
|
|
80
|
+
if not self._entries:
|
|
81
|
+
return
|
|
82
|
+
_id, entry = self._entries.popitem(last=False)
|
|
83
|
+
self._current_tokens -= entry.token_count
|
|
84
|
+
self._compressed_summaries.append(entry)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""记忆查询关键词分词"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Set
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def tokenize(text: str) -> Set[str]:
|
|
8
|
+
if not text:
|
|
9
|
+
return set()
|
|
10
|
+
text = text.lower()
|
|
11
|
+
# 按非字母数字字符分割
|
|
12
|
+
tokens = set(re.findall(r'[a-zA-Z0-9一-鿿]+', text))
|
|
13
|
+
# 去掉单字词(无意义)
|
|
14
|
+
return {t for t in tokens if len(t) > 1 if not t.isascii() or len(t) > 2}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def matches(content: str, query_tokens: Set[str]) -> bool:
|
|
18
|
+
if not query_tokens:
|
|
19
|
+
return True
|
|
20
|
+
content_lower = content.lower()
|
|
21
|
+
return any(t in content_lower for t in query_tokens)
|
voxcli/plan/__init__.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""执行计划 - 包含一组有依赖关系的任务"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import List, Optional, Dict, Set
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
from .task import Task, TaskStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlanStatus(Enum):
|
|
12
|
+
CREATED = "CREATED"
|
|
13
|
+
RUNNING = "RUNNING"
|
|
14
|
+
COMPLETED = "COMPLETED"
|
|
15
|
+
FAILED = "FAILED"
|
|
16
|
+
CANCELLED = "CANCELLED"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExecutionPlan:
|
|
20
|
+
def __init__(self, id: str, goal: str):
|
|
21
|
+
self._id = id
|
|
22
|
+
self._goal = goal
|
|
23
|
+
self._tasks: Dict[str, Task] = OrderedDict()
|
|
24
|
+
self._execution_order: List[str] = []
|
|
25
|
+
self._status = PlanStatus.CREATED
|
|
26
|
+
self._summary: Optional[str] = None
|
|
27
|
+
self._start_time: float = 0.0
|
|
28
|
+
self._end_time: float = 0.0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def id(self) -> str:
|
|
32
|
+
return self._id
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def goal(self) -> str:
|
|
36
|
+
return self._goal
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def status(self) -> PlanStatus:
|
|
40
|
+
return self._status
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def summary(self) -> Optional[str]:
|
|
44
|
+
return self._summary
|
|
45
|
+
|
|
46
|
+
@summary.setter
|
|
47
|
+
def summary(self, value: str):
|
|
48
|
+
self._summary = value
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def start_time(self) -> float:
|
|
52
|
+
return self._start_time
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def end_time(self) -> float:
|
|
56
|
+
return self._end_time
|
|
57
|
+
|
|
58
|
+
def add_task(self, task: Task):
|
|
59
|
+
self._tasks[task.id] = task
|
|
60
|
+
for dep_id in task.dependencies:
|
|
61
|
+
dep = self._tasks.get(dep_id)
|
|
62
|
+
if dep is not None:
|
|
63
|
+
dep.add_dependent(task.id)
|
|
64
|
+
|
|
65
|
+
def get_task(self, id: str) -> Optional[Task]:
|
|
66
|
+
return self._tasks.get(id)
|
|
67
|
+
|
|
68
|
+
def get_all_tasks(self) -> List[Task]:
|
|
69
|
+
return list(self._tasks.values())
|
|
70
|
+
|
|
71
|
+
def get_root_tasks(self) -> List[Task]:
|
|
72
|
+
return [t for t in self._tasks.values() if not t.dependencies]
|
|
73
|
+
|
|
74
|
+
def get_executable_tasks(self) -> List[Task]:
|
|
75
|
+
return [t for t in self._tasks.values() if t.is_executable(self._tasks)]
|
|
76
|
+
|
|
77
|
+
def compute_execution_order(self) -> bool:
|
|
78
|
+
self._execution_order.clear()
|
|
79
|
+
visited: Set[str] = set()
|
|
80
|
+
visiting: Set[str] = set()
|
|
81
|
+
|
|
82
|
+
def topo_sort(task: Task) -> bool:
|
|
83
|
+
if task.id in visiting:
|
|
84
|
+
return False
|
|
85
|
+
if task.id in visited:
|
|
86
|
+
return True
|
|
87
|
+
visiting.add(task.id)
|
|
88
|
+
for dep_id in task.dependencies:
|
|
89
|
+
dep = self._tasks.get(dep_id)
|
|
90
|
+
if dep is not None:
|
|
91
|
+
if not topo_sort(dep):
|
|
92
|
+
return False
|
|
93
|
+
visiting.remove(task.id)
|
|
94
|
+
visited.add(task.id)
|
|
95
|
+
self._execution_order.append(task.id)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
for task in self._tasks.values():
|
|
99
|
+
if task.id not in visited:
|
|
100
|
+
if not topo_sort(task):
|
|
101
|
+
return False
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def get_execution_order(self) -> List[str]:
|
|
105
|
+
if not self._execution_order:
|
|
106
|
+
self.compute_execution_order()
|
|
107
|
+
return list(self._execution_order)
|
|
108
|
+
|
|
109
|
+
def get_progress(self) -> float:
|
|
110
|
+
if not self._tasks:
|
|
111
|
+
return 1.0
|
|
112
|
+
completed = sum(1 for t in self._tasks.values()
|
|
113
|
+
if t.status == TaskStatus.COMPLETED)
|
|
114
|
+
return completed / len(self._tasks)
|
|
115
|
+
|
|
116
|
+
def is_all_completed(self) -> bool:
|
|
117
|
+
return all(t.status == TaskStatus.COMPLETED
|
|
118
|
+
for t in self._tasks.values())
|
|
119
|
+
|
|
120
|
+
def has_failed(self) -> bool:
|
|
121
|
+
return any(t.status == TaskStatus.FAILED
|
|
122
|
+
for t in self._tasks.values())
|
|
123
|
+
|
|
124
|
+
def mark_started(self):
|
|
125
|
+
self._status = PlanStatus.RUNNING
|
|
126
|
+
self._start_time = time.time()
|
|
127
|
+
|
|
128
|
+
def mark_completed(self):
|
|
129
|
+
self._status = PlanStatus.COMPLETED
|
|
130
|
+
self._end_time = time.time()
|
|
131
|
+
|
|
132
|
+
def mark_failed(self):
|
|
133
|
+
self._status = PlanStatus.FAILED
|
|
134
|
+
self._end_time = time.time()
|
|
135
|
+
|
|
136
|
+
def mark_cancelled(self):
|
|
137
|
+
self._status = PlanStatus.CANCELLED
|
|
138
|
+
self._end_time = time.time()
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def duration(self) -> float:
|
|
142
|
+
if self._start_time == 0:
|
|
143
|
+
return 0.0
|
|
144
|
+
if self._end_time == 0:
|
|
145
|
+
return time.time() - self._start_time
|
|
146
|
+
return self._end_time - self._start_time
|
|
147
|
+
|
|
148
|
+
def visualize(self) -> str:
|
|
149
|
+
lines = []
|
|
150
|
+
lines.append("╔══════════════════════════════════════════════════════════╗")
|
|
151
|
+
goal_display = self._goal[:46] + "..." if len(self._goal) > 46 else self._goal
|
|
152
|
+
lines.append(f"║ 执行计划: {goal_display:<46}║")
|
|
153
|
+
lines.append("╠══════════════════════════════════════════════════════════╣")
|
|
154
|
+
|
|
155
|
+
order = self.get_execution_order()
|
|
156
|
+
for i, task_id in enumerate(order):
|
|
157
|
+
task = self._tasks[task_id]
|
|
158
|
+
icon = self._status_icon(task.status)
|
|
159
|
+
deps = ",".join(task.dependencies) if task.dependencies else "无"
|
|
160
|
+
lines.append(f"║ {i+1}. {icon} {task._id:<20} [{task.type.value:<10}] 依赖: {deps:<15}║")
|
|
161
|
+
desc = task.description[:47] + "..." if len(task.description) > 50 else task.description
|
|
162
|
+
lines.append(f"║ {desc:<53}║")
|
|
163
|
+
|
|
164
|
+
lines.append("╚══════════════════════════════════════════════════════════╝")
|
|
165
|
+
lines.append(f" 进度: {self.get_progress()*100:.0f}% | 状态: {self._status.value}")
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
def summarize(self) -> str:
|
|
169
|
+
batches = self.get_execution_batches()
|
|
170
|
+
ready = self.get_executable_tasks()
|
|
171
|
+
lines = ["📋 计划摘要",
|
|
172
|
+
f" - 目标: {self._compact_goal(48)}",
|
|
173
|
+
f" - 任务数: {len(self._tasks)} | 并行批次: {len(batches)} | "
|
|
174
|
+
f"当前可执行: {len(ready)} | 状态: {self._status.value}"]
|
|
175
|
+
if batches:
|
|
176
|
+
lines.append(f" - 首批执行: {self._format_task_list(batches[0], 5)}")
|
|
177
|
+
if len(batches) > 1:
|
|
178
|
+
lines.append(f" - 最终收敛: {self._format_task_list(batches[-1], 5)}")
|
|
179
|
+
return "\n".join(lines)
|
|
180
|
+
|
|
181
|
+
def get_execution_batches(self) -> List[List[Task]]:
|
|
182
|
+
if not self._tasks:
|
|
183
|
+
return []
|
|
184
|
+
remaining = OrderedDict(self._tasks)
|
|
185
|
+
completed: Set[str] = set()
|
|
186
|
+
batches = []
|
|
187
|
+
while remaining:
|
|
188
|
+
batch = [t for t in remaining.values()
|
|
189
|
+
if completed.issuperset(t.dependencies)]
|
|
190
|
+
if not batch:
|
|
191
|
+
break
|
|
192
|
+
batches.append(batch)
|
|
193
|
+
for t in batch:
|
|
194
|
+
remaining.pop(t.id)
|
|
195
|
+
completed.add(t.id)
|
|
196
|
+
return batches
|
|
197
|
+
|
|
198
|
+
def _compact_goal(self, max_length: int) -> str:
|
|
199
|
+
goal = self._goal.replace("\r\n", " ").replace("\r", " ").replace("\n", " ").strip()
|
|
200
|
+
import re
|
|
201
|
+
goal = re.sub(r" {2,}", " ", goal)
|
|
202
|
+
if len(goal) <= max_length:
|
|
203
|
+
return goal
|
|
204
|
+
return goal[:max_length - 3] + "..."
|
|
205
|
+
|
|
206
|
+
def _format_task_list(self, batch: List[Task], limit: int) -> str:
|
|
207
|
+
if not batch:
|
|
208
|
+
return "无"
|
|
209
|
+
ids = [t.id for t in batch]
|
|
210
|
+
if len(ids) <= limit:
|
|
211
|
+
return ", ".join(ids)
|
|
212
|
+
return ", ".join(ids[:limit]) + f" 等 {len(ids)} 个任务"
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _status_icon(status: TaskStatus) -> str:
|
|
216
|
+
return {
|
|
217
|
+
TaskStatus.PENDING: "⏳",
|
|
218
|
+
TaskStatus.RUNNING: "▶️",
|
|
219
|
+
TaskStatus.COMPLETED: "✅",
|
|
220
|
+
TaskStatus.FAILED: "❌",
|
|
221
|
+
TaskStatus.SKIPPED: "⏭️",
|
|
222
|
+
}.get(status, "⏳")
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
return f"ExecutionPlan[{self._id}: {self._goal}] ({len(self._tasks)} tasks, {self._status.value})"
|