python-library-ai-agent 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.
- ai_agent/__init__.py +66 -0
- ai_agent/agent.py +122 -0
- ai_agent/app/__init__.py +10 -0
- ai_agent/app/_workspace.py +127 -0
- ai_agent/app/app.py +321 -0
- ai_agent/app/harness_io.py +109 -0
- ai_agent/app/output_format.py +77 -0
- ai_agent/app/packet.py +39 -0
- ai_agent/app/session.py +742 -0
- ai_agent/app/session_store.py +85 -0
- ai_agent/builtin_tools/__init__.py +18 -0
- ai_agent/builtin_tools/current_time.py +39 -0
- ai_agent/builtin_tools/pack.py +20 -0
- ai_agent/builtin_tools/prefix.py +11 -0
- ai_agent/context.py +151 -0
- ai_agent/harness/__init__.py +3 -0
- ai_agent/harness/current_time.py +25 -0
- ai_agent/harness/harness.py +324 -0
- ai_agent/harness/process.py +115 -0
- ai_agent/harness/prompts.py +38 -0
- ai_agent/harness/sandbox.py +139 -0
- ai_agent/json_extract.py +70 -0
- ai_agent/listener.py +172 -0
- ai_agent/llm.py +39 -0
- ai_agent/llm_openai.py +117 -0
- ai_agent/loop.py +124 -0
- ai_agent/mcp_config.py +54 -0
- ai_agent/mcp_loader.py +110 -0
- ai_agent/memory/__init__.py +9 -0
- ai_agent/memory/compression_work.py +71 -0
- ai_agent/memory/compressor.py +339 -0
- ai_agent/memory/config.py +40 -0
- ai_agent/memory/context_builder.py +57 -0
- ai_agent/memory/memory_system.py +561 -0
- ai_agent/memory/models.py +76 -0
- ai_agent/memory/snapshot_merge.py +158 -0
- ai_agent/memory/store.py +107 -0
- ai_agent/memory/worker.py +227 -0
- ai_agent/plan/__init__.py +15 -0
- ai_agent/plan/complete.py +64 -0
- ai_agent/plan/delivery.py +41 -0
- ai_agent/plan/display.py +46 -0
- ai_agent/plan/models.py +44 -0
- ai_agent/plan/parse.py +39 -0
- ai_agent/plan/planner.py +204 -0
- ai_agent/plan/runner.py +281 -0
- ai_agent/react_tool_turn.py +39 -0
- ai_agent/rule/__init__.py +3 -0
- ai_agent/rule/rules.py +36 -0
- ai_agent/skill/__init__.py +5 -0
- ai_agent/skill/builtin_registry.py +56 -0
- ai_agent/skill/catalog.py +104 -0
- ai_agent/skill/frontmatter.py +83 -0
- ai_agent/skill/manager.py +486 -0
- ai_agent/skill/models.py +31 -0
- ai_agent/skill/roots.py +150 -0
- ai_agent/skill/skill_kit.py +80 -0
- ai_agent/skill/tool_declarations.py +68 -0
- ai_agent/tools.py +123 -0
- python_library_ai_agent-0.1.0.dist-info/METADATA +10 -0
- python_library_ai_agent-0.1.0.dist-info/RECORD +62 -0
- python_library_ai_agent-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ai_agent.context import ChatMessage
|
|
7
|
+
|
|
8
|
+
_CONVERSATION_FILE = "conversation.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def conversation_path(session_root: Path) -> Path:
|
|
12
|
+
"""会话根目录下的对话持久化文件路径。"""
|
|
13
|
+
return session_root / _CONVERSATION_FILE
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_conversation(session_root: Path) -> list[ChatMessage]:
|
|
17
|
+
"""
|
|
18
|
+
从磁盘读取对话历史。
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
session_root: 会话工作区根
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
无文件或解析失败时返回空列表
|
|
25
|
+
"""
|
|
26
|
+
path = conversation_path(session_root)
|
|
27
|
+
if not path.is_file():
|
|
28
|
+
return []
|
|
29
|
+
try:
|
|
30
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
31
|
+
except (json.JSONDecodeError, OSError):
|
|
32
|
+
return []
|
|
33
|
+
if not isinstance(raw, list):
|
|
34
|
+
return []
|
|
35
|
+
messages: list[ChatMessage] = []
|
|
36
|
+
for item in raw:
|
|
37
|
+
if not isinstance(item, dict):
|
|
38
|
+
continue
|
|
39
|
+
role = item.get("role")
|
|
40
|
+
content = item.get("content")
|
|
41
|
+
if role not in ("system", "user", "assistant", "tool"):
|
|
42
|
+
continue
|
|
43
|
+
if not isinstance(content, str):
|
|
44
|
+
continue
|
|
45
|
+
name = item.get("name")
|
|
46
|
+
if name is not None and not isinstance(name, str):
|
|
47
|
+
name = None
|
|
48
|
+
messages.append(
|
|
49
|
+
ChatMessage(
|
|
50
|
+
role=role,
|
|
51
|
+
content=content,
|
|
52
|
+
name=name,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
return messages
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_conversation(session_root: Path, messages: list[ChatMessage]) -> None:
|
|
59
|
+
"""
|
|
60
|
+
将会话对话历史写入磁盘。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
session_root: 会话工作区根
|
|
64
|
+
messages: 待持久化的消息列表
|
|
65
|
+
"""
|
|
66
|
+
session_root.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
payload = [
|
|
68
|
+
{
|
|
69
|
+
"role": m.role,
|
|
70
|
+
"content": m.content,
|
|
71
|
+
**({"name": m.name} if m.name else {}),
|
|
72
|
+
}
|
|
73
|
+
for m in messages
|
|
74
|
+
]
|
|
75
|
+
conversation_path(session_root).write_text(
|
|
76
|
+
json.dumps(payload, ensure_ascii=False, indent=2),
|
|
77
|
+
encoding="utf-8",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def clear_conversation(session_root: Path) -> None:
|
|
82
|
+
"""删除会话对话持久化文件(若存在)。"""
|
|
83
|
+
path = conversation_path(session_root)
|
|
84
|
+
if path.is_file():
|
|
85
|
+
path.unlink()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ai_agent.builtin_tools.current_time import (
|
|
4
|
+
CURRENT_TIME_SHORT_NAME,
|
|
5
|
+
build_current_time_tool,
|
|
6
|
+
harness_current_time_tool_name,
|
|
7
|
+
)
|
|
8
|
+
from ai_agent.builtin_tools.pack import build_app_builtin_tools
|
|
9
|
+
from ai_agent.builtin_tools.prefix import APP_BUILTIN_PREFIX, builtin_tool_name
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"APP_BUILTIN_PREFIX",
|
|
13
|
+
"CURRENT_TIME_SHORT_NAME",
|
|
14
|
+
"build_app_builtin_tools",
|
|
15
|
+
"build_current_time_tool",
|
|
16
|
+
"builtin_tool_name",
|
|
17
|
+
"harness_current_time_tool_name",
|
|
18
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ai_agent.builtin_tools.prefix import builtin_tool_name
|
|
6
|
+
from ai_agent.harness.current_time import get_current_time
|
|
7
|
+
from ai_agent.tools import Tool
|
|
8
|
+
|
|
9
|
+
CURRENT_TIME_SHORT_NAME = "current_time"
|
|
10
|
+
|
|
11
|
+
_CURRENT_TIME_PARAMETERS: dict[str, Any] = {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"timezone": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "IANA 时区名,如 Asia/Shanghai;省略则使用本机本地时区",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"additionalProperties": False,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def harness_current_time_tool_name() -> str:
|
|
24
|
+
"""Harness 沙箱层同名工具的对外名,用于去重。"""
|
|
25
|
+
return "harness__current_time"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_current_time_tool() -> Tool:
|
|
29
|
+
"""构造 ``builtin__current_time``,不依赖 Harness 工作区。"""
|
|
30
|
+
return Tool(
|
|
31
|
+
name=builtin_tool_name(CURRENT_TIME_SHORT_NAME),
|
|
32
|
+
description="获取当前日期与时间(ISO 8601),可按 IANA 时区名指定时区。",
|
|
33
|
+
parameters=_CURRENT_TIME_PARAMETERS,
|
|
34
|
+
handler=_run_current_time,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_current_time(*, timezone: str = "") -> str:
|
|
39
|
+
return get_current_time(timezone)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ai_agent.builtin_tools.current_time import build_current_time_tool
|
|
4
|
+
from ai_agent.tools import Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_app_builtin_tools(*, current_time: bool) -> list[Tool]:
|
|
8
|
+
"""
|
|
9
|
+
组装 AgentApp 可选的应用级内置工具(与 Harness 沙箱无关)。
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
current_time: 是否注册 ``builtin__current_time``
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
待并入 ToolRegistry 基础层的工具列表
|
|
16
|
+
"""
|
|
17
|
+
tools: list[Tool] = []
|
|
18
|
+
if current_time:
|
|
19
|
+
tools.append(build_current_time_tool())
|
|
20
|
+
return tools
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
APP_BUILTIN_PREFIX = "builtin"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def builtin_tool_name(short: str) -> str:
|
|
7
|
+
"""生成应用级内置工具对外名,形如 ``builtin__current_time``。"""
|
|
8
|
+
key = short.strip()
|
|
9
|
+
if not key:
|
|
10
|
+
raise ValueError("内置工具短名不能为空")
|
|
11
|
+
return f"{APP_BUILTIN_PREFIX}__{key}"
|
ai_agent/context.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ai_agent.llm import LLMClient
|
|
11
|
+
from ai_agent.listener import AgentListener
|
|
12
|
+
from ai_agent.tools import ToolRegistry
|
|
13
|
+
|
|
14
|
+
MessageRole = Literal["system", "user", "assistant", "tool"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RunStatus(str, Enum):
|
|
18
|
+
"""一轮运行的整体状态。"""
|
|
19
|
+
|
|
20
|
+
RUNNING = "running"
|
|
21
|
+
COMPLETED = "completed"
|
|
22
|
+
FAILED = "failed"
|
|
23
|
+
MAX_STEPS = "max_steps"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RunPhaseKind(str, Enum):
|
|
27
|
+
"""监听回调所归属的运行阶段,供前端区分规划与逐步执行。"""
|
|
28
|
+
|
|
29
|
+
PLANNING = "planning"
|
|
30
|
+
STEP = "step"
|
|
31
|
+
DIRECT = "direct"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RunPhase:
|
|
36
|
+
"""
|
|
37
|
+
单次 ReAct 或规划补全所处的阶段。
|
|
38
|
+
|
|
39
|
+
规划阶段仅 ``kind=PLANNING``;逐步执行时带 ``step_index`` 与 ``step_id``。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
kind: RunPhaseKind
|
|
43
|
+
step_index: int | None = None
|
|
44
|
+
step_id: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AgentContext:
|
|
49
|
+
"""
|
|
50
|
+
Agent 运行环境:语言模型、工具表与 ReAct 步数上限。
|
|
51
|
+
|
|
52
|
+
由 ``Agent`` 构造时创建;测试可替换 ``llm`` 等成员。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
llm: LLMClient
|
|
56
|
+
tools: ToolRegistry
|
|
57
|
+
max_steps: int = 20
|
|
58
|
+
listeners: list[AgentListener] = field(default_factory=list)
|
|
59
|
+
on_run_begin: Callable[["RunContext"], None] | None = None
|
|
60
|
+
on_run_end: Callable[["RunContext"], None] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ChatMessage:
|
|
65
|
+
"""对话消息;用于历史记录与用户输入。"""
|
|
66
|
+
|
|
67
|
+
role: MessageRole
|
|
68
|
+
content: str
|
|
69
|
+
name: str | None = None
|
|
70
|
+
|
|
71
|
+
def to_api(self) -> dict[str, Any]:
|
|
72
|
+
item: dict[str, Any] = {"role": self.role, "content": self.content}
|
|
73
|
+
if self.name:
|
|
74
|
+
item["name"] = self.name
|
|
75
|
+
return item
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class ToolInvocation:
|
|
80
|
+
"""按顺序记录的一次工具调用;运行中更新思考与回答。"""
|
|
81
|
+
|
|
82
|
+
call_id: str
|
|
83
|
+
tool_name: str
|
|
84
|
+
arguments: dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
thinking: str = ""
|
|
86
|
+
answer: str = ""
|
|
87
|
+
ok: bool = True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class RunContext:
|
|
92
|
+
"""
|
|
93
|
+
单轮运行的上下文:输入、按序工具调用与最终输出。
|
|
94
|
+
|
|
95
|
+
运行上下文:系统提示与 messages;运行后读取 output 与 tool_invocations。
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
system_prompt: str = ""
|
|
99
|
+
ephemeral_skill_context: str = ""
|
|
100
|
+
messages: list[ChatMessage] = field(default_factory=list)
|
|
101
|
+
tool_invocations: list[ToolInvocation] = field(default_factory=list)
|
|
102
|
+
tool_turns: list[list[ToolInvocation]] = field(default_factory=list)
|
|
103
|
+
thinking: str = ""
|
|
104
|
+
output: str = ""
|
|
105
|
+
status: RunStatus = RunStatus.RUNNING
|
|
106
|
+
phase: RunPhase | None = None
|
|
107
|
+
def api_messages(self) -> list[dict[str, Any]]:
|
|
108
|
+
"""组装送入语言模型的消息列表。"""
|
|
109
|
+
out: list[dict[str, Any]] = []
|
|
110
|
+
system = self._effective_system_prompt()
|
|
111
|
+
if system:
|
|
112
|
+
out.append({"role": "system", "content": system})
|
|
113
|
+
out.extend(m.to_api() for m in self.messages)
|
|
114
|
+
for turn in self.tool_turns:
|
|
115
|
+
tool_calls: list[dict[str, Any]] = []
|
|
116
|
+
for inv in turn:
|
|
117
|
+
tool_calls.append(
|
|
118
|
+
{
|
|
119
|
+
"id": inv.call_id,
|
|
120
|
+
"type": "function",
|
|
121
|
+
"function": {
|
|
122
|
+
"name": inv.tool_name,
|
|
123
|
+
"arguments": json.dumps(inv.arguments, ensure_ascii=False),
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
assistant: dict[str, Any] = {
|
|
128
|
+
"role": "assistant",
|
|
129
|
+
"content": turn[0].thinking or None,
|
|
130
|
+
"tool_calls": tool_calls,
|
|
131
|
+
}
|
|
132
|
+
out.append(assistant)
|
|
133
|
+
for inv in turn:
|
|
134
|
+
if inv.answer:
|
|
135
|
+
out.append(
|
|
136
|
+
{
|
|
137
|
+
"role": "tool",
|
|
138
|
+
"tool_call_id": inv.call_id,
|
|
139
|
+
"content": inv.answer,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
def _effective_system_prompt(self) -> str:
|
|
145
|
+
base = self.system_prompt.strip()
|
|
146
|
+
extra = self.ephemeral_skill_context.strip()
|
|
147
|
+
if base and extra:
|
|
148
|
+
return f"{base}\n\n{extra}"
|
|
149
|
+
if extra:
|
|
150
|
+
return extra
|
|
151
|
+
return base
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_current_time(timezone: str = "") -> str:
|
|
8
|
+
"""
|
|
9
|
+
返回当前日期与时间。
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
timezone: IANA 时区名,如 Asia/Shanghai;留空则用本机本地时区
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
ISO 8601 格式的时间字符串
|
|
16
|
+
"""
|
|
17
|
+
if timezone.strip():
|
|
18
|
+
try:
|
|
19
|
+
tz = ZoneInfo(timezone.strip())
|
|
20
|
+
except ZoneInfoNotFoundError as exc:
|
|
21
|
+
raise ValueError(f"未知时区: {timezone}") from exc
|
|
22
|
+
now = datetime.now(tz)
|
|
23
|
+
else:
|
|
24
|
+
now = datetime.now().astimezone()
|
|
25
|
+
return now.isoformat(timespec="seconds")
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ai_agent.harness.current_time import get_current_time
|
|
8
|
+
from ai_agent.harness.process import run_python, run_shell
|
|
9
|
+
from ai_agent.harness.sandbox import HarnessSandbox
|
|
10
|
+
from ai_agent.skill.manager import SkillManager
|
|
11
|
+
from ai_agent.skill.skill_kit import SkillKit
|
|
12
|
+
from ai_agent.tools import Tool
|
|
13
|
+
|
|
14
|
+
_HARNESS_TOOL_PREFIX = "harness"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Harness:
|
|
18
|
+
"""
|
|
19
|
+
隔离工作区上的文件与进程能力;与对话代理核分离,由调用方构造后导出为工具列表。
|
|
20
|
+
|
|
21
|
+
路径均相对工作区根;可挂载只读技能区。工具说明与返回值不向模型暴露宿主机绝对路径。
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
workspace: 沙箱工作区根目录
|
|
25
|
+
skill_roots: 只读技能仓库根;与 skill_kit 二选一
|
|
26
|
+
skill_kit: 已构造的 SkillKit;与 skill_roots 二选一
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
workspace: Path | str,
|
|
32
|
+
*,
|
|
33
|
+
skill_roots: (
|
|
34
|
+
Mapping[str, Path | str] | Sequence[Path | str] | Path | str | None
|
|
35
|
+
) = None,
|
|
36
|
+
skill_kit: SkillKit | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
if skill_kit is not None and skill_roots is not None:
|
|
39
|
+
raise ValueError("skill_kit 与 skill_roots 不可同时指定")
|
|
40
|
+
self._sandbox = HarnessSandbox(Path(workspace))
|
|
41
|
+
self._prefix = _HARNESS_TOOL_PREFIX
|
|
42
|
+
self._skill_kit: SkillKit | None = None
|
|
43
|
+
if skill_kit is not None:
|
|
44
|
+
self._skill_kit = skill_kit
|
|
45
|
+
elif skill_roots is not None:
|
|
46
|
+
self._skill_kit = SkillKit(skill_roots)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def workspace(self) -> Path:
|
|
50
|
+
"""沙箱工作区根目录(供应用侧配置与测试使用,勿写入模型可见文案)。"""
|
|
51
|
+
return self._sandbox.root
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def skill(self) -> SkillKit:
|
|
55
|
+
"""已配置的 SkillKit;未配置 skill_roots 时访问会报错。"""
|
|
56
|
+
if self._skill_kit is None:
|
|
57
|
+
raise ValueError("未配置 skill_roots")
|
|
58
|
+
return self._skill_kit
|
|
59
|
+
|
|
60
|
+
def _tool_name(self, short: str) -> str:
|
|
61
|
+
return f"{self._prefix}__{short}"
|
|
62
|
+
|
|
63
|
+
def read_file(self, path: str, offset: int = 1, limit: int = 0) -> str:
|
|
64
|
+
"""
|
|
65
|
+
读取工作区内文本文件。
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
path: 相对工作区根的路径
|
|
69
|
+
offset: 起始行号,从 1 起
|
|
70
|
+
limit: 最多读取行数;0 表示读到末尾
|
|
71
|
+
"""
|
|
72
|
+
return self._sandbox.read_text_file(path, offset=offset, limit=limit)
|
|
73
|
+
|
|
74
|
+
def write_file(self, path: str, content: str, append: bool = False) -> str:
|
|
75
|
+
"""
|
|
76
|
+
写入或追加工作区内文本文件。
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
path: 相对工作区根的路径
|
|
80
|
+
content: 文件内容
|
|
81
|
+
append: 为 True 时追加
|
|
82
|
+
"""
|
|
83
|
+
return self._sandbox.write_text_file(path, content, append=append)
|
|
84
|
+
|
|
85
|
+
def list_files(
|
|
86
|
+
self,
|
|
87
|
+
path: str = "",
|
|
88
|
+
max_entries: int = 200,
|
|
89
|
+
pattern: str = "",
|
|
90
|
+
) -> str:
|
|
91
|
+
"""
|
|
92
|
+
扫描工作区内文件与目录。
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
path: 相对工作区根的子目录;留空则扫描整个工作区
|
|
96
|
+
max_entries: 最多返回条数
|
|
97
|
+
pattern: 可选 glob,如 *.py
|
|
98
|
+
"""
|
|
99
|
+
entries = self._sandbox.list_entries(
|
|
100
|
+
path,
|
|
101
|
+
max_entries=max_entries,
|
|
102
|
+
pattern=pattern,
|
|
103
|
+
)
|
|
104
|
+
if not entries:
|
|
105
|
+
return "(空)"
|
|
106
|
+
truncated = len(entries) >= max_entries
|
|
107
|
+
lines = "\n".join(entries)
|
|
108
|
+
if truncated:
|
|
109
|
+
lines += f"\n...(已达上限 {max_entries} 条)"
|
|
110
|
+
return lines
|
|
111
|
+
|
|
112
|
+
def run_shell(self, command: str, cwd: str = "", timeout_seconds: int = 0) -> str:
|
|
113
|
+
"""
|
|
114
|
+
在工作区内执行 shell 命令。
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
command: shell 命令
|
|
118
|
+
cwd: 相对工作区的子目录;留空则用工作区根
|
|
119
|
+
timeout_seconds: 超时秒数;0 则用默认
|
|
120
|
+
"""
|
|
121
|
+
work_dir = (
|
|
122
|
+
self._sandbox.root
|
|
123
|
+
if not cwd.strip()
|
|
124
|
+
else self._sandbox.resolve_path(cwd)
|
|
125
|
+
)
|
|
126
|
+
if not work_dir.is_dir():
|
|
127
|
+
raise ValueError(f"cwd 不是目录: {cwd or '.'}")
|
|
128
|
+
return run_shell(
|
|
129
|
+
command,
|
|
130
|
+
work_dir=work_dir,
|
|
131
|
+
timeout_seconds=timeout_seconds,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def run_python(self, code: str, timeout_seconds: int = 0) -> str:
|
|
135
|
+
"""
|
|
136
|
+
在工作区根目录下执行 Python 代码片段。
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
code: Python 源码
|
|
140
|
+
timeout_seconds: 超时秒数;0 则用默认
|
|
141
|
+
"""
|
|
142
|
+
return run_python(
|
|
143
|
+
code,
|
|
144
|
+
work_dir=self._sandbox.root,
|
|
145
|
+
timeout_seconds=timeout_seconds,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def workspace_info(self) -> str:
|
|
149
|
+
"""说明隔离工作区约束(不包含宿主机绝对路径)。"""
|
|
150
|
+
return (
|
|
151
|
+
"隔离工作区已启用。所有路径均相对于工作区根;"
|
|
152
|
+
"无法读取或写入工作区外的文件。"
|
|
153
|
+
"使用 list_files 查看目录结构。"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def current_time(self, timezone: str = "") -> str:
|
|
157
|
+
"""
|
|
158
|
+
返回当前日期与时间。
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
timezone: IANA 时区名,如 Asia/Shanghai;留空则用本机本地时区
|
|
162
|
+
"""
|
|
163
|
+
return get_current_time(timezone)
|
|
164
|
+
|
|
165
|
+
def build_skill_tools(self) -> list[Tool]:
|
|
166
|
+
"""生成 skill 管理 Tool 列表;须已配置 skill_roots。"""
|
|
167
|
+
return self.skill.build_management_tools()
|
|
168
|
+
|
|
169
|
+
def build_all_tools(self) -> list[Tool]:
|
|
170
|
+
"""合并沙箱、skill 管理与已启用子工具(未配置 skill_roots 时仅沙箱)。"""
|
|
171
|
+
tools = self.build_tools()
|
|
172
|
+
if self._skill_kit is not None:
|
|
173
|
+
tools = tools + self._skill_kit.build_all_flat_tools()
|
|
174
|
+
return tools
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def skill_manager(self) -> SkillManager:
|
|
178
|
+
"""已配置的 SkillManager;未配置 skill_roots 时访问会报错。"""
|
|
179
|
+
return self.skill.manager
|
|
180
|
+
|
|
181
|
+
def build_tools(self) -> list[Tool]:
|
|
182
|
+
"""生成沙箱区 Tool 列表。"""
|
|
183
|
+
specs: list[tuple[str, str, dict[str, Any], Any]] = [
|
|
184
|
+
(
|
|
185
|
+
"read_file",
|
|
186
|
+
"读取隔离工作区内文本文件,返回带行号的内容。",
|
|
187
|
+
{
|
|
188
|
+
"type": "object",
|
|
189
|
+
"properties": {
|
|
190
|
+
"path": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "相对工作区根的文件路径",
|
|
193
|
+
},
|
|
194
|
+
"offset": {
|
|
195
|
+
"type": "integer",
|
|
196
|
+
"description": "起始行号,从 1 起,默认 1",
|
|
197
|
+
},
|
|
198
|
+
"limit": {
|
|
199
|
+
"type": "integer",
|
|
200
|
+
"description": "最多读取行数;0 表示读到末尾",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"required": ["path"],
|
|
204
|
+
"additionalProperties": False,
|
|
205
|
+
},
|
|
206
|
+
self.read_file,
|
|
207
|
+
),
|
|
208
|
+
(
|
|
209
|
+
"write_file",
|
|
210
|
+
"写入或追加隔离工作区内文本文件,必要时创建父目录。",
|
|
211
|
+
{
|
|
212
|
+
"type": "object",
|
|
213
|
+
"properties": {
|
|
214
|
+
"path": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"description": "相对工作区根的文件路径",
|
|
217
|
+
},
|
|
218
|
+
"content": {"type": "string", "description": "要写入的文本"},
|
|
219
|
+
"append": {
|
|
220
|
+
"type": "boolean",
|
|
221
|
+
"description": "为 true 时追加,否则覆盖",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
"required": ["path", "content"],
|
|
225
|
+
"additionalProperties": False,
|
|
226
|
+
},
|
|
227
|
+
self.write_file,
|
|
228
|
+
),
|
|
229
|
+
(
|
|
230
|
+
"list_files",
|
|
231
|
+
"扫描隔离工作区内的文件与目录,仅返回相对路径。",
|
|
232
|
+
{
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"path": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "相对工作区根的子目录;留空扫描整个工作区",
|
|
238
|
+
},
|
|
239
|
+
"max_entries": {
|
|
240
|
+
"type": "integer",
|
|
241
|
+
"description": "最多返回条数,默认 200",
|
|
242
|
+
},
|
|
243
|
+
"pattern": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"description": "可选 glob,如 *.txt",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
"additionalProperties": False,
|
|
249
|
+
},
|
|
250
|
+
self.list_files,
|
|
251
|
+
),
|
|
252
|
+
(
|
|
253
|
+
"run_shell",
|
|
254
|
+
"在隔离工作区内执行 shell 命令,返回退出码与标准输出/错误。",
|
|
255
|
+
{
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": {
|
|
258
|
+
"command": {"type": "string", "description": "shell 命令"},
|
|
259
|
+
"cwd": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"description": "相对工作区的子目录;留空则用工作区根",
|
|
262
|
+
},
|
|
263
|
+
"timeout_seconds": {
|
|
264
|
+
"type": "integer",
|
|
265
|
+
"description": "超时秒数,默认 120,上限 600",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
"required": ["command"],
|
|
269
|
+
"additionalProperties": False,
|
|
270
|
+
},
|
|
271
|
+
self.run_shell,
|
|
272
|
+
),
|
|
273
|
+
(
|
|
274
|
+
"run_python",
|
|
275
|
+
"在隔离工作区根目录下执行 Python 代码片段。",
|
|
276
|
+
{
|
|
277
|
+
"type": "object",
|
|
278
|
+
"properties": {
|
|
279
|
+
"code": {"type": "string", "description": "Python 源码"},
|
|
280
|
+
"timeout_seconds": {
|
|
281
|
+
"type": "integer",
|
|
282
|
+
"description": "超时秒数,默认 120,上限 600",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
"required": ["code"],
|
|
286
|
+
"additionalProperties": False,
|
|
287
|
+
},
|
|
288
|
+
self.run_python,
|
|
289
|
+
),
|
|
290
|
+
(
|
|
291
|
+
"workspace_info",
|
|
292
|
+
"查看隔离工作区的使用约束。",
|
|
293
|
+
{
|
|
294
|
+
"type": "object",
|
|
295
|
+
"properties": {},
|
|
296
|
+
"additionalProperties": False,
|
|
297
|
+
},
|
|
298
|
+
self.workspace_info,
|
|
299
|
+
),
|
|
300
|
+
(
|
|
301
|
+
"current_time",
|
|
302
|
+
"获取当前日期与时间,可按 IANA 时区名指定时区。",
|
|
303
|
+
{
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {
|
|
306
|
+
"timezone": {
|
|
307
|
+
"type": "string",
|
|
308
|
+
"description": "IANA 时区名,如 Asia/Shanghai;省略则使用本机本地时区",
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
"additionalProperties": False,
|
|
312
|
+
},
|
|
313
|
+
self.current_time,
|
|
314
|
+
),
|
|
315
|
+
]
|
|
316
|
+
return [
|
|
317
|
+
Tool(
|
|
318
|
+
name=self._tool_name(short),
|
|
319
|
+
description=description,
|
|
320
|
+
parameters=parameters,
|
|
321
|
+
handler=handler,
|
|
322
|
+
)
|
|
323
|
+
for short, description, parameters, handler in specs
|
|
324
|
+
]
|