illusion-code 0.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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
协调器模式模块
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
本模块提供任务通知的 XML 序列化/反序列化功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- TaskNotification: 已完成任务的结果结构
|
|
9
|
+
- format_task_notification: 序列化为 XML
|
|
10
|
+
- parse_task_notification: 从 XML 解析
|
|
11
|
+
|
|
12
|
+
使用示例:
|
|
13
|
+
>>> from illusion.coordinator.coordinator_mode import TaskNotification, format_task_notification
|
|
14
|
+
>>> n = TaskNotification(task_id="agent-1", status="completed", summary="Done")
|
|
15
|
+
>>> xml = format_task_notification(n)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# TaskNotification 数据类
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TaskNotification:
|
|
32
|
+
"""已完成代理任务的结构化结果。
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
task_id: 任务 ID
|
|
36
|
+
status: 状态 (completed/failed/killed)
|
|
37
|
+
summary: 人类可读的状态摘要
|
|
38
|
+
result: 代理的最终文本响应 (可选)
|
|
39
|
+
usage: 使用统计信息 (可选)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
task_id: str
|
|
43
|
+
"""任务 ID。"""
|
|
44
|
+
|
|
45
|
+
status: str
|
|
46
|
+
"""状态 (completed/failed/killed)。"""
|
|
47
|
+
|
|
48
|
+
summary: str
|
|
49
|
+
"""人类可读的状态摘要。"""
|
|
50
|
+
|
|
51
|
+
result: Optional[str] = None
|
|
52
|
+
"""代理的最终文本响应。"""
|
|
53
|
+
|
|
54
|
+
usage: Optional[dict[str, int]] = None
|
|
55
|
+
"""使用统计信息。"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# XML 序列化/反序列化
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# 使用统计字段名
|
|
63
|
+
_USAGE_FIELDS = ("total_tokens", "tool_uses", "duration_ms")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def format_task_notification(n: TaskNotification) -> str:
|
|
67
|
+
"""将 TaskNotification 序列化为标准 XML envelope。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
n: 任务通知对象
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: XML 格式的字符串
|
|
74
|
+
"""
|
|
75
|
+
parts = [
|
|
76
|
+
"<task-notification>",
|
|
77
|
+
f"<task-id>{n.task_id}</task-id>",
|
|
78
|
+
f"<status>{n.status}</status>",
|
|
79
|
+
f"<summary>{n.summary}</summary>",
|
|
80
|
+
]
|
|
81
|
+
if n.result is not None:
|
|
82
|
+
parts.append(f"<result>{n.result}</result>")
|
|
83
|
+
if n.usage:
|
|
84
|
+
parts.append("<usage>")
|
|
85
|
+
for key in _USAGE_FIELDS:
|
|
86
|
+
if key in n.usage:
|
|
87
|
+
parts.append(f" <{key}>{n.usage[key]}</{key}>")
|
|
88
|
+
parts.append("</usage>")
|
|
89
|
+
parts.append("</task-notification>")
|
|
90
|
+
return "\n".join(parts)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_task_notification(xml: str) -> TaskNotification:
|
|
94
|
+
"""从 XML 字符串解析 TaskNotification。
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
xml: XML 格式的字符串
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
TaskNotification: 解析后的任务通知对象
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def _extract(tag: str) -> Optional[str]:
|
|
104
|
+
m = re.search(rf"<{tag}>(.*?)</{tag}>", xml, re.DOTALL)
|
|
105
|
+
return m.group(1).strip() if m else None
|
|
106
|
+
|
|
107
|
+
task_id = _extract("task-id") or ""
|
|
108
|
+
status = _extract("status") or ""
|
|
109
|
+
summary = _extract("summary") or ""
|
|
110
|
+
result = _extract("result")
|
|
111
|
+
|
|
112
|
+
usage: Optional[dict[str, int]] = None
|
|
113
|
+
usage_block = re.search(r"<usage>(.*?)</usage>", xml, re.DOTALL)
|
|
114
|
+
if usage_block:
|
|
115
|
+
usage = {}
|
|
116
|
+
for key in _USAGE_FIELDS:
|
|
117
|
+
m = re.search(rf"<{key}>(\d+)</{key}>", usage_block.group(1))
|
|
118
|
+
if m:
|
|
119
|
+
usage[key] = int(m.group(1))
|
|
120
|
+
|
|
121
|
+
return TaskNotification(
|
|
122
|
+
task_id=task_id,
|
|
123
|
+
status=status,
|
|
124
|
+
summary=summary,
|
|
125
|
+
result=result,
|
|
126
|
+
usage=usage,
|
|
127
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
核心引擎模块
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
本模块提供 IllusionCode 核心引擎的导出接口。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- ConversationMessage: 对话消息模型
|
|
9
|
+
- TextBlock: 文本内容块
|
|
10
|
+
- ToolUseBlock: 工具调用块
|
|
11
|
+
- ToolResultBlock: 工具结果块
|
|
12
|
+
- QueryEngine: 查询引擎
|
|
13
|
+
- AssistantTextDelta: 助手文本增量事件
|
|
14
|
+
- AssistantTurnComplete: 助手轮次完成事件
|
|
15
|
+
- ToolExecutionStarted: 工具执行开始事件
|
|
16
|
+
- ToolExecutionCompleted: 工具执行完成事件
|
|
17
|
+
|
|
18
|
+
使用示例:
|
|
19
|
+
>>> from illusion.engine import ConversationMessage, QueryEngine
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING: # 类型检查时导入,避免循环依赖
|
|
27
|
+
from illusion.engine.messages import (
|
|
28
|
+
ConversationMessage,
|
|
29
|
+
TextBlock,
|
|
30
|
+
ToolResultBlock,
|
|
31
|
+
ToolUseBlock,
|
|
32
|
+
)
|
|
33
|
+
from illusion.engine.query_engine import QueryEngine
|
|
34
|
+
from illusion.engine.stream_events import (
|
|
35
|
+
AssistantTextDelta,
|
|
36
|
+
AssistantTurnComplete,
|
|
37
|
+
ToolExecutionCompleted,
|
|
38
|
+
ToolExecutionStarted,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AssistantTextDelta",
|
|
43
|
+
"AssistantTurnComplete",
|
|
44
|
+
"ConversationMessage",
|
|
45
|
+
"QueryEngine",
|
|
46
|
+
"TextBlock",
|
|
47
|
+
"ToolExecutionCompleted",
|
|
48
|
+
"ToolExecutionStarted",
|
|
49
|
+
"ToolResultBlock",
|
|
50
|
+
"ToolUseBlock",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def __getattr__(name: str):
|
|
55
|
+
if name in {"ConversationMessage", "TextBlock", "ToolResultBlock", "ToolUseBlock"}:
|
|
56
|
+
from illusion.engine.messages import (
|
|
57
|
+
ConversationMessage,
|
|
58
|
+
TextBlock,
|
|
59
|
+
ToolResultBlock,
|
|
60
|
+
ToolUseBlock,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"ConversationMessage": ConversationMessage,
|
|
65
|
+
"TextBlock": TextBlock,
|
|
66
|
+
"ToolResultBlock": ToolResultBlock,
|
|
67
|
+
"ToolUseBlock": ToolUseBlock,
|
|
68
|
+
}[name]
|
|
69
|
+
|
|
70
|
+
if name == "QueryEngine":
|
|
71
|
+
from illusion.engine.query_engine import QueryEngine
|
|
72
|
+
|
|
73
|
+
return QueryEngine
|
|
74
|
+
|
|
75
|
+
if name in {
|
|
76
|
+
"AssistantTextDelta",
|
|
77
|
+
"AssistantTurnComplete",
|
|
78
|
+
"ToolExecutionCompleted",
|
|
79
|
+
"ToolExecutionStarted",
|
|
80
|
+
}:
|
|
81
|
+
from illusion.engine.stream_events import (
|
|
82
|
+
AssistantTextDelta,
|
|
83
|
+
AssistantTurnComplete,
|
|
84
|
+
ToolExecutionCompleted,
|
|
85
|
+
ToolExecutionStarted,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"AssistantTextDelta": AssistantTextDelta,
|
|
90
|
+
"AssistantTurnComplete": AssistantTurnComplete,
|
|
91
|
+
"ToolExecutionCompleted": ToolExecutionCompleted,
|
|
92
|
+
"ToolExecutionStarted": ToolExecutionStarted,
|
|
93
|
+
}[name]
|
|
94
|
+
|
|
95
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""简单的使用量聚合。
|
|
2
|
+
|
|
3
|
+
本模块提供成本跟踪功能,用于累积会话期间的使用量统计。
|
|
4
|
+
|
|
5
|
+
主要类:
|
|
6
|
+
- CostTracker: 使用量累积器
|
|
7
|
+
|
|
8
|
+
使用示例:
|
|
9
|
+
>>> from illusion.engine.cost_tracker import CostTracker
|
|
10
|
+
>>> tracker = CostTracker()
|
|
11
|
+
>>> tracker.add(usage_snapshot)
|
|
12
|
+
>>> print(tracker.total)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from illusion.api.usage import UsageSnapshot
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CostTracker:
|
|
21
|
+
"""在整个会话期间累积使用量。
|
|
22
|
+
|
|
23
|
+
用于跟踪对话过程中的令牌使用情况,包括输入和输出令牌数。
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
total: 累积的总使用量(只读属性)
|
|
27
|
+
|
|
28
|
+
使用示例:
|
|
29
|
+
>>> tracker = CostTracker()
|
|
30
|
+
>>> tracker.add(UsageSnapshot(input_tokens=100, output_tokens=50))
|
|
31
|
+
>>> print(tracker.total.input_tokens) # 100
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self._usage = UsageSnapshot() # 初始化使用量快照
|
|
36
|
+
|
|
37
|
+
def add(self, usage: UsageSnapshot) -> None:
|
|
38
|
+
"""将使用量快照添加到运行总和。
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
usage: 要添加的使用量快照
|
|
42
|
+
"""
|
|
43
|
+
self._usage = UsageSnapshot(
|
|
44
|
+
input_tokens=self._usage.input_tokens + usage.input_tokens,
|
|
45
|
+
output_tokens=self._usage.output_tokens + usage.output_tokens,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def total(self) -> UsageSnapshot:
|
|
50
|
+
"""返回聚合后的使用量。
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
UsageSnapshot: 累积的使用量快照
|
|
54
|
+
"""
|
|
55
|
+
return self._usage
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
对话消息模型模块
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供查询引擎使用的对话消息模型。
|
|
6
|
+
|
|
7
|
+
主要类:
|
|
8
|
+
- TextBlock: 纯文本内容块
|
|
9
|
+
- ToolUseBlock: 模型执行命名工具的请求
|
|
10
|
+
- ToolResultBlock: 发送回模型的工具结果内容
|
|
11
|
+
- ConversationMessage: 单个助手或用户消息
|
|
12
|
+
|
|
13
|
+
使用示例:
|
|
14
|
+
>>> from illusion.engine.messages import ConversationMessage, TextBlock
|
|
15
|
+
>>> msg = ConversationMessage.from_user_text("Hello")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Annotated, Literal
|
|
21
|
+
from uuid import uuid4
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TextBlock(BaseModel):
|
|
27
|
+
"""纯文本内容块
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
type: 块类型(固定为 "text")
|
|
31
|
+
text: 文本内容
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
type: Literal["text"] = "text"
|
|
35
|
+
text: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ToolUseBlock(BaseModel):
|
|
39
|
+
"""模型执行命名工具的请求
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
type: 块类型(固定为 "tool_use")
|
|
43
|
+
id: 工具调用唯一标识
|
|
44
|
+
name: 工具名称
|
|
45
|
+
input: 工具输入参数
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
type: Literal["tool_use"] = "tool_use"
|
|
49
|
+
id: str = Field(default_factory=lambda: f"toolu_{uuid4().hex}")
|
|
50
|
+
name: str
|
|
51
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ToolResultBlock(BaseModel):
|
|
55
|
+
"""发送回模型的工具结果内容
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
type: 块类型(固定为 "tool_result")
|
|
59
|
+
tool_use_id: 对应的工具调用 ID
|
|
60
|
+
content: 工具返回的内容(纯文本或内容块列表)
|
|
61
|
+
is_error: 是否为错误结果
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
type: Literal["tool_result"] = "tool_result"
|
|
65
|
+
tool_use_id: str
|
|
66
|
+
content: str | list[ContentBlock] = ""
|
|
67
|
+
is_error: bool = False
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def text_content(self) -> str:
|
|
71
|
+
"""返回纯文本内容。
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: 文本字符串(content 为列表时提取 TextBlock 的文本)
|
|
75
|
+
"""
|
|
76
|
+
if isinstance(self.content, str):
|
|
77
|
+
return self.content
|
|
78
|
+
return "".join(
|
|
79
|
+
block.text for block in self.content if isinstance(block, TextBlock)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ThinkingBlock(BaseModel):
|
|
84
|
+
"""思考内容块(Anthropic extended thinking / DeepSeek thinking mode)
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
type: 块类型(固定为 "thinking")
|
|
88
|
+
thinking: 思考文本
|
|
89
|
+
signature: 加密签名(Anthropic API 需要回传以验证思考内容未被篡改)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
type: Literal["thinking"] = "thinking"
|
|
93
|
+
thinking: str
|
|
94
|
+
signature: str = ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MediaBlock(BaseModel):
|
|
98
|
+
"""图片文件内容块。
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
type: 块类型(固定为 "media")
|
|
102
|
+
file_path: 文件绝对路径
|
|
103
|
+
media_type: MIME 类型,如 "image/png"
|
|
104
|
+
data: base64 编码的文件数据
|
|
105
|
+
metadata: 额外信息(文件大小等)
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
type: Literal["media"] = "media"
|
|
109
|
+
file_path: str
|
|
110
|
+
media_type: str
|
|
111
|
+
data: str = ""
|
|
112
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _build_tool_result_content(
|
|
116
|
+
output: str,
|
|
117
|
+
metadata: dict[str, Any],
|
|
118
|
+
) -> str | list[ContentBlock]:
|
|
119
|
+
"""从工具输出和元数据构建 ToolResultBlock 的内容。
|
|
120
|
+
|
|
121
|
+
如果元数据中包含媒体信息,返回 TextBlock + MediaBlock 的列表;
|
|
122
|
+
否则返回原始文本。
|
|
123
|
+
"""
|
|
124
|
+
if "media_category" not in metadata:
|
|
125
|
+
return output
|
|
126
|
+
|
|
127
|
+
media_block = MediaBlock(
|
|
128
|
+
file_path=metadata.get("media_path", ""),
|
|
129
|
+
media_type=metadata.get("media_type", "application/octet-stream"),
|
|
130
|
+
data=metadata.get("media_data", ""),
|
|
131
|
+
metadata={"size": metadata.get("media_size", 0)},
|
|
132
|
+
)
|
|
133
|
+
return [TextBlock(text=output), media_block]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _messages_have_media(messages: list[ConversationMessage]) -> bool:
|
|
137
|
+
"""检查消息列表中是否包含 MediaBlock"""
|
|
138
|
+
for msg in messages:
|
|
139
|
+
for block in msg.content:
|
|
140
|
+
if isinstance(block, MediaBlock):
|
|
141
|
+
return True
|
|
142
|
+
if isinstance(block, ToolResultBlock) and isinstance(block.content, list):
|
|
143
|
+
if any(isinstance(b, MediaBlock) for b in block.content):
|
|
144
|
+
return True
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _strip_media_from_messages(messages: list[ConversationMessage]) -> list[ConversationMessage]:
|
|
149
|
+
"""将消息中的 MediaBlock 替换为文本描述,用于不支持图片的模型优雅降级"""
|
|
150
|
+
result: list[ConversationMessage] = []
|
|
151
|
+
for msg in messages:
|
|
152
|
+
new_blocks: list[Any] = []
|
|
153
|
+
for block in msg.content:
|
|
154
|
+
if isinstance(block, MediaBlock):
|
|
155
|
+
size_str = f" ({block.metadata['size']} bytes)" if "size" in block.metadata else ""
|
|
156
|
+
new_blocks.append(TextBlock(
|
|
157
|
+
text=f"[image file: {block.file_path}{size_str}, {block.media_type}] "
|
|
158
|
+
"This model does not support image input",
|
|
159
|
+
))
|
|
160
|
+
elif isinstance(block, ToolResultBlock) and isinstance(block.content, list):
|
|
161
|
+
stripped: list[Any] = []
|
|
162
|
+
for b in block.content:
|
|
163
|
+
if isinstance(b, MediaBlock):
|
|
164
|
+
size_str = f" ({b.metadata['size']} bytes)" if "size" in b.metadata else ""
|
|
165
|
+
stripped.append(TextBlock(
|
|
166
|
+
text=f"[image file: {b.file_path}{size_str}, {b.media_type}] "
|
|
167
|
+
"This model does not support image input",
|
|
168
|
+
))
|
|
169
|
+
else:
|
|
170
|
+
stripped.append(b)
|
|
171
|
+
new_blocks.append(ToolResultBlock(
|
|
172
|
+
tool_use_id=block.tool_use_id,
|
|
173
|
+
content=stripped,
|
|
174
|
+
is_error=block.is_error,
|
|
175
|
+
))
|
|
176
|
+
else:
|
|
177
|
+
new_blocks.append(block)
|
|
178
|
+
result.append(ConversationMessage(role=msg.role, content=new_blocks))
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# 内容块联合类型
|
|
183
|
+
ContentBlock = Annotated[
|
|
184
|
+
TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock | MediaBlock,
|
|
185
|
+
Field(discriminator="type"),
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ConversationMessage(BaseModel):
|
|
190
|
+
"""单个助手或用户消息
|
|
191
|
+
|
|
192
|
+
Attributes:
|
|
193
|
+
role: 消息角色("user" 或 "assistant")
|
|
194
|
+
content: 内容块列表
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
role: Literal["user", "assistant"]
|
|
198
|
+
content: list[ContentBlock] = Field(default_factory=list)
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def from_user_text(cls, text: str) -> "ConversationMessage":
|
|
202
|
+
"""从原始文本构造用户消息
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
text: 用户输入文本
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
ConversationMessage: 用户消息
|
|
209
|
+
"""
|
|
210
|
+
return cls(role="user", content=[TextBlock(text=text)])
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def text(self) -> str:
|
|
214
|
+
"""返回连接的文本块
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
str: 所有文本块的连接字符串
|
|
218
|
+
"""
|
|
219
|
+
return "".join(
|
|
220
|
+
block.text for block in self.content if isinstance(block, TextBlock)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def tool_uses(self) -> list[ToolUseBlock]:
|
|
225
|
+
"""返回消息中包含的所有工具调用
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
list[ToolUseBlock]: 工具调用列表
|
|
229
|
+
"""
|
|
230
|
+
return [block for block in self.content if isinstance(block, ToolUseBlock)]
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def thinking_text(self) -> str:
|
|
234
|
+
"""返回连接后的思考文本块。"""
|
|
235
|
+
return "\n\n".join(
|
|
236
|
+
block.thinking.strip()
|
|
237
|
+
for block in self.content
|
|
238
|
+
if isinstance(block, ThinkingBlock) and block.thinking.strip()
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def to_api_param(self, *, provider_type: str = "anthropic") -> dict[str, Any]:
|
|
242
|
+
"""将消息转换为提供商 SDK 消息参数。
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
provider_type: 提供商类型
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
dict[str, Any]: API 参数格式的字典
|
|
249
|
+
"""
|
|
250
|
+
content_blocks = list(self.content)
|
|
251
|
+
|
|
252
|
+
# MiMo 等 API 要求 Text block 的 text 字段最小长度为 1
|
|
253
|
+
# 过滤掉空文本块,避免 "content or tool_calls must be set" 错误
|
|
254
|
+
if self.role == "assistant":
|
|
255
|
+
content_blocks = [
|
|
256
|
+
b for b in content_blocks
|
|
257
|
+
if not (isinstance(b, TextBlock) and not b.text.strip())
|
|
258
|
+
]
|
|
259
|
+
# 如果过滤后没有内容,添加占位符
|
|
260
|
+
if not content_blocks:
|
|
261
|
+
content_blocks = [TextBlock(text="...")]
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
"role": self.role,
|
|
265
|
+
"content": [serialize_content_block(block, provider_type=provider_type) for block in content_blocks],
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def serialize_content_block(block: ContentBlock, *, provider_type: str = "anthropic") -> dict[str, Any]:
|
|
270
|
+
"""将本地内容块转换为提供商线格式。
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
block: 内容块
|
|
274
|
+
provider_type: 提供商类型("anthropic"、"openai_compat"、"openai_codex")
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
dict[str, Any]: 线格式字典
|
|
278
|
+
"""
|
|
279
|
+
if isinstance(block, TextBlock):
|
|
280
|
+
return {"type": "text", "text": block.text}
|
|
281
|
+
|
|
282
|
+
if isinstance(block, ToolUseBlock):
|
|
283
|
+
return {
|
|
284
|
+
"type": "tool_use",
|
|
285
|
+
"id": block.id,
|
|
286
|
+
"name": block.name,
|
|
287
|
+
"input": block.input,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if isinstance(block, ThinkingBlock):
|
|
291
|
+
result: dict[str, Any] = {"type": "thinking", "thinking": block.thinking}
|
|
292
|
+
if block.signature:
|
|
293
|
+
result["signature"] = block.signature
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
if isinstance(block, MediaBlock):
|
|
297
|
+
return _serialize_media_block(block, provider_type)
|
|
298
|
+
|
|
299
|
+
# tool_result
|
|
300
|
+
if isinstance(block.content, list):
|
|
301
|
+
serialized_content = [
|
|
302
|
+
serialize_content_block(inner, provider_type=provider_type)
|
|
303
|
+
for inner in block.content
|
|
304
|
+
]
|
|
305
|
+
else:
|
|
306
|
+
serialized_content = block.content
|
|
307
|
+
return {
|
|
308
|
+
"type": "tool_result",
|
|
309
|
+
"tool_use_id": block.tool_use_id,
|
|
310
|
+
"content": serialized_content,
|
|
311
|
+
"is_error": block.is_error,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _serialize_media_block(block: MediaBlock, provider_type: str) -> dict[str, Any]:
|
|
316
|
+
"""将图片 MediaBlock 按提供商格式序列化。"""
|
|
317
|
+
if provider_type == "anthropic":
|
|
318
|
+
return {
|
|
319
|
+
"type": "image",
|
|
320
|
+
"source": {
|
|
321
|
+
"type": "base64",
|
|
322
|
+
"media_type": block.media_type,
|
|
323
|
+
"data": block.data,
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
if provider_type == "openai_codex":
|
|
327
|
+
return {
|
|
328
|
+
"type": "input_image",
|
|
329
|
+
"image_url": f"data:{block.media_type};base64,{block.data}",
|
|
330
|
+
}
|
|
331
|
+
# openai_compat
|
|
332
|
+
return {
|
|
333
|
+
"type": "image_url",
|
|
334
|
+
"image_url": {"url": f"data:{block.media_type};base64,{block.data}"},
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def assistant_message_from_api(raw_message: Any) -> ConversationMessage:
|
|
339
|
+
"""将 Anthropic SDK 消息对象转换为对话消息
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
raw_message: Anthropic SDK 原始消息
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
ConversationMessage: 转换后的对话消息
|
|
346
|
+
"""
|
|
347
|
+
content: list[ContentBlock] = []
|
|
348
|
+
|
|
349
|
+
for raw_block in getattr(raw_message, "content", []):
|
|
350
|
+
block_type = getattr(raw_block, "type", None)
|
|
351
|
+
if block_type == "text":
|
|
352
|
+
content.append(TextBlock(text=getattr(raw_block, "text", "")))
|
|
353
|
+
elif block_type == "tool_use":
|
|
354
|
+
content.append(
|
|
355
|
+
ToolUseBlock(
|
|
356
|
+
id=getattr(raw_block, "id", f"toolu_{uuid4().hex}"),
|
|
357
|
+
name=getattr(raw_block, "name", ""),
|
|
358
|
+
input=dict(getattr(raw_block, "input", {}) or {}),
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
elif block_type == "thinking":
|
|
362
|
+
content.append(
|
|
363
|
+
ThinkingBlock(
|
|
364
|
+
thinking=getattr(raw_block, "thinking", ""),
|
|
365
|
+
signature=getattr(raw_block, "signature", "") or "",
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return ConversationMessage(role="assistant", content=content)
|