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
ai_agent/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from ai_agent.agent import Agent
|
|
2
|
+
from ai_agent.app import AgentApp, AgentSession, RunInputPacket, RunOutputPacket
|
|
3
|
+
from ai_agent.harness import Harness
|
|
4
|
+
from ai_agent.memory import BuiltMemoryContext, MemoryConfig, MemorySystem
|
|
5
|
+
from ai_agent.skill import BuiltinToolRegistry, SkillKit, SkillManager
|
|
6
|
+
from ai_agent.context import (
|
|
7
|
+
AgentContext,
|
|
8
|
+
ChatMessage,
|
|
9
|
+
RunContext,
|
|
10
|
+
RunPhase,
|
|
11
|
+
RunPhaseKind,
|
|
12
|
+
RunStatus,
|
|
13
|
+
ToolInvocation,
|
|
14
|
+
)
|
|
15
|
+
from ai_agent.listener import AgentListener
|
|
16
|
+
from ai_agent.mcp_config import McpConfig, McpStdioServerConfig, parse_mcp_config
|
|
17
|
+
from ai_agent.mcp_loader import MCPToolLoader
|
|
18
|
+
from ai_agent.plan import (
|
|
19
|
+
Plan,
|
|
20
|
+
PlanParseError,
|
|
21
|
+
parse_plan_text,
|
|
22
|
+
PlanPlanner,
|
|
23
|
+
PlanRunResult,
|
|
24
|
+
PlanRunner,
|
|
25
|
+
PlanStep,
|
|
26
|
+
PlanStepFailedError,
|
|
27
|
+
)
|
|
28
|
+
from ai_agent.rule import RuleSet
|
|
29
|
+
from ai_agent.tools import Tool
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Agent",
|
|
33
|
+
"AgentApp",
|
|
34
|
+
"AgentSession",
|
|
35
|
+
"RunInputPacket",
|
|
36
|
+
"RunOutputPacket",
|
|
37
|
+
"BuiltMemoryContext",
|
|
38
|
+
"Harness",
|
|
39
|
+
"MemoryConfig",
|
|
40
|
+
"MemorySystem",
|
|
41
|
+
"BuiltinToolRegistry",
|
|
42
|
+
"SkillKit",
|
|
43
|
+
"SkillManager",
|
|
44
|
+
"AgentContext",
|
|
45
|
+
"ChatMessage",
|
|
46
|
+
"AgentListener",
|
|
47
|
+
"McpConfig",
|
|
48
|
+
"McpStdioServerConfig",
|
|
49
|
+
"MCPToolLoader",
|
|
50
|
+
"RunContext",
|
|
51
|
+
"RunPhase",
|
|
52
|
+
"RunPhaseKind",
|
|
53
|
+
"RunStatus",
|
|
54
|
+
"Tool",
|
|
55
|
+
"ToolInvocation",
|
|
56
|
+
"parse_mcp_config",
|
|
57
|
+
"RuleSet",
|
|
58
|
+
"Plan",
|
|
59
|
+
"PlanParseError",
|
|
60
|
+
"parse_plan_text",
|
|
61
|
+
"PlanPlanner",
|
|
62
|
+
"PlanRunResult",
|
|
63
|
+
"PlanRunner",
|
|
64
|
+
"PlanStep",
|
|
65
|
+
"PlanStepFailedError",
|
|
66
|
+
]
|
ai_agent/agent.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from openai import AsyncOpenAI
|
|
7
|
+
|
|
8
|
+
from ai_agent.context import (
|
|
9
|
+
AgentContext,
|
|
10
|
+
ChatMessage,
|
|
11
|
+
RunContext,
|
|
12
|
+
RunPhase,
|
|
13
|
+
RunPhaseKind,
|
|
14
|
+
)
|
|
15
|
+
from ai_agent.listener import AgentListener, normalize_listeners
|
|
16
|
+
from ai_agent.llm import LLMClient
|
|
17
|
+
from ai_agent.llm_openai import OpenAILLM
|
|
18
|
+
from ai_agent.loop import ReactLoop
|
|
19
|
+
from ai_agent.rule import RuleSet
|
|
20
|
+
from ai_agent.tools import Tool, ToolRegistry
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Agent:
|
|
24
|
+
"""
|
|
25
|
+
ReAct 运行入口:构造时组装 ``AgentContext`` 并驱动多步循环。
|
|
26
|
+
|
|
27
|
+
系统提示由构造时的 ``rule_paths`` 指定文件读入;``run`` 只接收消息列表。
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
api_key: str,
|
|
34
|
+
model: str,
|
|
35
|
+
base_url: str,
|
|
36
|
+
temperature: float | None = None,
|
|
37
|
+
max_tokens: int | None = None,
|
|
38
|
+
thinking_enabled: bool = False,
|
|
39
|
+
tools: list[Tool] | ToolRegistry | None = None,
|
|
40
|
+
rule_paths: list[str] | list[Path] | tuple[str, ...] | tuple[Path, ...] | None = None,
|
|
41
|
+
max_steps: int = 20,
|
|
42
|
+
listeners: AgentListener | Iterable[AgentListener] | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
if not api_key.strip():
|
|
45
|
+
raise ValueError("api_key 不能为空")
|
|
46
|
+
if not model.strip():
|
|
47
|
+
raise ValueError("model 不能为空")
|
|
48
|
+
if not base_url.strip():
|
|
49
|
+
raise ValueError("base_url 不能为空")
|
|
50
|
+
|
|
51
|
+
api_key = api_key.strip()
|
|
52
|
+
model = model.strip()
|
|
53
|
+
base_url = base_url.strip().rstrip("/")
|
|
54
|
+
|
|
55
|
+
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
56
|
+
llm: LLMClient = OpenAILLM(
|
|
57
|
+
client,
|
|
58
|
+
model=model,
|
|
59
|
+
base_url=base_url,
|
|
60
|
+
temperature=temperature,
|
|
61
|
+
max_tokens=max_tokens,
|
|
62
|
+
thinking_enabled=thinking_enabled,
|
|
63
|
+
)
|
|
64
|
+
if isinstance(tools, ToolRegistry):
|
|
65
|
+
registry = tools
|
|
66
|
+
else:
|
|
67
|
+
registry = ToolRegistry(tools)
|
|
68
|
+
self.context = AgentContext(
|
|
69
|
+
llm=llm,
|
|
70
|
+
tools=registry,
|
|
71
|
+
max_steps=max_steps,
|
|
72
|
+
listeners=normalize_listeners(listeners),
|
|
73
|
+
)
|
|
74
|
+
self._rules = RuleSet(rule_paths)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def rules(self) -> RuleSet:
|
|
78
|
+
"""构造时绑定的规则集。"""
|
|
79
|
+
return self._rules
|
|
80
|
+
|
|
81
|
+
async def run(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
messages: list[ChatMessage],
|
|
85
|
+
) -> str:
|
|
86
|
+
"""
|
|
87
|
+
运行一轮 ReAct 循环,返回最终回答文本。
|
|
88
|
+
"""
|
|
89
|
+
return await self.run_with_system(
|
|
90
|
+
system_prompt=self._rules.build_system_prompt(),
|
|
91
|
+
messages=messages,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async def run_with_system(
|
|
95
|
+
self,
|
|
96
|
+
*,
|
|
97
|
+
system_prompt: str,
|
|
98
|
+
messages: list[ChatMessage],
|
|
99
|
+
phase: RunPhase | None = None,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""
|
|
102
|
+
使用给定系统提示运行一轮(供会话层叠加计划步说明等)。
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
system_prompt: 本轮送入模型的完整系统提示
|
|
106
|
+
messages: 用户与助手历史及本轮输入
|
|
107
|
+
"""
|
|
108
|
+
run = RunContext(
|
|
109
|
+
system_prompt=system_prompt,
|
|
110
|
+
messages=list(messages),
|
|
111
|
+
phase=phase or RunPhase(kind=RunPhaseKind.DIRECT),
|
|
112
|
+
)
|
|
113
|
+
if self.context.on_run_begin is not None:
|
|
114
|
+
self.context.on_run_begin(run)
|
|
115
|
+
try:
|
|
116
|
+
loop = ReactLoop(self.context)
|
|
117
|
+
async for updated in loop.run(run):
|
|
118
|
+
run = updated
|
|
119
|
+
return run.output
|
|
120
|
+
finally:
|
|
121
|
+
if self.context.on_run_end is not None:
|
|
122
|
+
self.context.on_run_end(run)
|
ai_agent/app/__init__.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_SESSIONS_DIR = "sessions"
|
|
7
|
+
SESSION_HARNESS_SUBDIR = "harness"
|
|
8
|
+
SESSION_MEMORY_SUBDIR = "memory"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_app_sandbox_root(sandbox: Path | str) -> Path:
|
|
12
|
+
"""
|
|
13
|
+
解析并创建总沙箱根目录。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
sandbox: 总沙箱路径
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
规范化后的绝对路径
|
|
20
|
+
"""
|
|
21
|
+
root = Path(sandbox).expanduser().resolve()
|
|
22
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
if not root.is_dir():
|
|
24
|
+
raise ValueError(f"沙箱根须为目录: {root}")
|
|
25
|
+
return root
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_session_id(session_id: str) -> str:
|
|
29
|
+
"""
|
|
30
|
+
校验会话标识,拒绝路径穿越与分隔符。
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
session_id: 调用方提供的会话 id
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
去首尾空白后的 id
|
|
37
|
+
"""
|
|
38
|
+
cleaned = session_id.strip()
|
|
39
|
+
if not cleaned:
|
|
40
|
+
raise ValueError("session_id 不能为空")
|
|
41
|
+
if cleaned in (".", ".."):
|
|
42
|
+
raise ValueError(f"非法 session_id: {session_id}")
|
|
43
|
+
for char in ("/", "\\", "\0"):
|
|
44
|
+
if char in cleaned:
|
|
45
|
+
raise ValueError(f"session_id 不能包含路径分隔符: {session_id}")
|
|
46
|
+
return cleaned
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def session_workspace(sandbox_root: Path, session_id: str) -> Path:
|
|
50
|
+
"""
|
|
51
|
+
在总沙箱下为某会话分配子工作区目录。
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
sandbox_root: 总沙箱根(已 resolve)
|
|
55
|
+
session_id: 会话 id
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
该会话专属子目录(已创建)
|
|
59
|
+
"""
|
|
60
|
+
label = validate_session_id(session_id)
|
|
61
|
+
target = (sandbox_root / _SESSIONS_DIR / label).resolve()
|
|
62
|
+
try:
|
|
63
|
+
target.relative_to(sandbox_root)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise ValueError(f"会话工作区越出总沙箱: {session_id}") from exc
|
|
66
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
if not target.is_dir():
|
|
68
|
+
raise ValueError(f"无法创建会话工作区: {session_id}")
|
|
69
|
+
return target
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _validate_session_subdir_name(name: str, *, label: str) -> str:
|
|
73
|
+
cleaned = name.strip()
|
|
74
|
+
if not cleaned:
|
|
75
|
+
raise ValueError(f"{label} 不能为空")
|
|
76
|
+
if cleaned in (".", ".."):
|
|
77
|
+
raise ValueError(f"非法 {label}: {name}")
|
|
78
|
+
for char in ("/", "\\", "\0"):
|
|
79
|
+
if char in cleaned:
|
|
80
|
+
raise ValueError(f"{label} 不能包含路径分隔符: {name}")
|
|
81
|
+
return cleaned
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def session_harness_workspace(session_root: Path) -> Path:
|
|
85
|
+
"""
|
|
86
|
+
在会话工作区下分配 Harness 隔离子目录。
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session_root: 会话根目录(已 resolve)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Harness 工作区路径(已创建)
|
|
93
|
+
"""
|
|
94
|
+
root = session_root.resolve()
|
|
95
|
+
sub = _validate_session_subdir_name(
|
|
96
|
+
SESSION_HARNESS_SUBDIR,
|
|
97
|
+
label="harness 子目录",
|
|
98
|
+
)
|
|
99
|
+
target = (root / sub).resolve()
|
|
100
|
+
try:
|
|
101
|
+
target.relative_to(root)
|
|
102
|
+
except ValueError as exc:
|
|
103
|
+
raise ValueError("Harness 子目录越出会话工作区") from exc
|
|
104
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
if not target.is_dir():
|
|
106
|
+
raise ValueError("无法创建 Harness 工作区")
|
|
107
|
+
return target
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def reset_session_subdirs(session_root: Path) -> None:
|
|
111
|
+
"""
|
|
112
|
+
清空会话内 Harness 与 memory 子目录并重建。
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
session_root: 会话根目录
|
|
116
|
+
"""
|
|
117
|
+
root = session_root.resolve()
|
|
118
|
+
for sub_name in (SESSION_HARNESS_SUBDIR, SESSION_MEMORY_SUBDIR):
|
|
119
|
+
sub = _validate_session_subdir_name(sub_name, label="子目录")
|
|
120
|
+
target = (root / sub).resolve()
|
|
121
|
+
try:
|
|
122
|
+
target.relative_to(root)
|
|
123
|
+
except ValueError as exc:
|
|
124
|
+
raise ValueError(f"子目录越出会话工作区: {sub_name}") from exc
|
|
125
|
+
if target.is_dir():
|
|
126
|
+
shutil.rmtree(target)
|
|
127
|
+
target.mkdir(parents=True, exist_ok=True)
|
ai_agent/app/app.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ai_agent.app._workspace import (
|
|
7
|
+
SESSION_MEMORY_SUBDIR,
|
|
8
|
+
reset_session_subdirs,
|
|
9
|
+
resolve_app_sandbox_root,
|
|
10
|
+
session_harness_workspace,
|
|
11
|
+
session_workspace,
|
|
12
|
+
validate_session_id,
|
|
13
|
+
)
|
|
14
|
+
from ai_agent.app.harness_io import (
|
|
15
|
+
format_input_files_context,
|
|
16
|
+
resolve_output_files,
|
|
17
|
+
stage_input_files,
|
|
18
|
+
)
|
|
19
|
+
from ai_agent.app.output_format import parse_structured_run_output
|
|
20
|
+
from ai_agent.app.packet import RunInputPacket, RunOutputPacket
|
|
21
|
+
from ai_agent.app.session_store import (
|
|
22
|
+
clear_conversation,
|
|
23
|
+
load_conversation,
|
|
24
|
+
save_conversation,
|
|
25
|
+
)
|
|
26
|
+
from ai_agent.app.session import (
|
|
27
|
+
AgentSession,
|
|
28
|
+
build_session,
|
|
29
|
+
normalize_session_listeners,
|
|
30
|
+
)
|
|
31
|
+
from ai_agent.listener import AgentListener, notify_app_run_end
|
|
32
|
+
from ai_agent.memory import MemoryConfig, MemorySystem
|
|
33
|
+
from ai_agent.tools import Tool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentApp:
|
|
37
|
+
"""
|
|
38
|
+
多会话应用入口:在总沙箱下按会话标识分配子目录,并装配隔离工作区、技能与对话代理。
|
|
39
|
+
|
|
40
|
+
各会话下固定 harness 子目录供工具读写、memory 子目录供分层记忆;语言模型与记忆压缩
|
|
41
|
+
模型在 AgentApp 构造时配置一次,会话间工作区与对话历史相互隔离。
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
sandbox: 总沙箱根目录
|
|
45
|
+
skill_roots: 只读技能仓库根;键为根键,值为目录路径;未配置则不挂载技能
|
|
46
|
+
rule_paths: 系统规则文本文件路径列表,固定拼入各轮系统提示
|
|
47
|
+
api_key: 主对话语言模型 API 密钥
|
|
48
|
+
model: 主对话模型名
|
|
49
|
+
base_url: 主对话 API 地址
|
|
50
|
+
temperature: 主对话采样温度;未设则由服务端默认
|
|
51
|
+
max_tokens: 主对话单次补全 token 上限;未设则由服务端默认
|
|
52
|
+
thinking_enabled: 主对话是否向兼容接口开启思考模式(``enable_thinking``)
|
|
53
|
+
max_steps: 单轮 ReAct 最大步数
|
|
54
|
+
extra_tools: 各会话共用的额外工具(如 MCP),共享同一列表引用
|
|
55
|
+
listeners: 运行时事件监听;可传单个或序列
|
|
56
|
+
memory_api_key: 记忆压缩用模型 API 密钥;三者缺一则不启用记忆
|
|
57
|
+
memory_model: 记忆压缩用模型名
|
|
58
|
+
memory_base_url: 记忆压缩用 API 地址
|
|
59
|
+
memory_short_term_max_messages: 短期记忆条数上限
|
|
60
|
+
memory_short_term_overflow_batch: 每次从短期弹出的条数
|
|
61
|
+
memory_date_memory_days: 日期记忆保留天数
|
|
62
|
+
memory_date_memory_max_entries_per_day: 单日日期记忆条目上限
|
|
63
|
+
memory_long_term_max_chunks: 长期记忆块数上限
|
|
64
|
+
memory_important_max_entries: 重要记忆条目上限
|
|
65
|
+
harness_enabled: 是否向模型注册 Harness 沙箱工具;默认关闭
|
|
66
|
+
current_time_tool_enabled: 是否注册 ``builtin__current_time``;默认开启
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
sandbox: Path | str,
|
|
72
|
+
*,
|
|
73
|
+
skill_roots: (
|
|
74
|
+
Mapping[str, Path | str]
|
|
75
|
+
| Sequence[Path | str]
|
|
76
|
+
| Path
|
|
77
|
+
| str
|
|
78
|
+
| None
|
|
79
|
+
) = None,
|
|
80
|
+
rule_paths: Sequence[Path | str] | None = None,
|
|
81
|
+
api_key: str,
|
|
82
|
+
model: str,
|
|
83
|
+
base_url: str,
|
|
84
|
+
temperature: float | None = None,
|
|
85
|
+
max_tokens: int | None = None,
|
|
86
|
+
thinking_enabled: bool = False,
|
|
87
|
+
max_steps: int = 20,
|
|
88
|
+
extra_tools: list[Tool] | None = None,
|
|
89
|
+
listeners: AgentListener | Iterable[AgentListener] | None = None,
|
|
90
|
+
memory_api_key: str | None = None,
|
|
91
|
+
memory_model: str | None = None,
|
|
92
|
+
memory_base_url: str | None = None,
|
|
93
|
+
memory_short_term_max_messages: int = 20,
|
|
94
|
+
memory_short_term_overflow_batch: int = 5,
|
|
95
|
+
memory_date_memory_days: int = 7,
|
|
96
|
+
memory_date_memory_max_entries_per_day: int = 50,
|
|
97
|
+
memory_long_term_max_chunks: int = 30,
|
|
98
|
+
memory_important_max_entries: int = 20,
|
|
99
|
+
harness_enabled: bool = False,
|
|
100
|
+
current_time_tool_enabled: bool = True,
|
|
101
|
+
) -> None:
|
|
102
|
+
self._sandbox_root = resolve_app_sandbox_root(sandbox)
|
|
103
|
+
self._skill_roots = skill_roots
|
|
104
|
+
self._rule_paths = tuple(rule_paths) if rule_paths else None
|
|
105
|
+
self._api_key = api_key
|
|
106
|
+
self._model = model
|
|
107
|
+
self._base_url = base_url
|
|
108
|
+
self._temperature = temperature
|
|
109
|
+
self._max_tokens = max_tokens
|
|
110
|
+
self._thinking_enabled = thinking_enabled
|
|
111
|
+
self._max_steps = max_steps
|
|
112
|
+
self._shared_extra_tools: list[Tool] = list(extra_tools or [])
|
|
113
|
+
self._listeners = normalize_session_listeners(listeners)
|
|
114
|
+
self._memory_api_key = memory_api_key
|
|
115
|
+
self._memory_model = memory_model
|
|
116
|
+
self._memory_base_url = memory_base_url
|
|
117
|
+
self._memory_config = MemoryConfig(
|
|
118
|
+
short_term_max_messages=memory_short_term_max_messages,
|
|
119
|
+
short_term_overflow_batch=memory_short_term_overflow_batch,
|
|
120
|
+
date_memory_days=memory_date_memory_days,
|
|
121
|
+
date_memory_max_entries_per_day=memory_date_memory_max_entries_per_day,
|
|
122
|
+
long_term_max_chunks=memory_long_term_max_chunks,
|
|
123
|
+
important_max_entries=memory_important_max_entries,
|
|
124
|
+
)
|
|
125
|
+
self._harness_enabled = harness_enabled
|
|
126
|
+
self._current_time_tool_enabled = current_time_tool_enabled
|
|
127
|
+
self._sessions: dict[str, AgentSession] = {}
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def shared_extra_tools(self) -> list[Tool]:
|
|
131
|
+
"""各会话共用的已加载工具(如 MCP),运行时共享同一列表引用。"""
|
|
132
|
+
return self._shared_extra_tools
|
|
133
|
+
|
|
134
|
+
def set_shared_extra_tools(self, tools: list[Tool]) -> None:
|
|
135
|
+
"""
|
|
136
|
+
替换各会话共用的工具表(须在 ``run`` 之前完成,例如 MCP ``load`` 之后)。
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
tools: 新工具列表;会替换内部列表内容
|
|
140
|
+
"""
|
|
141
|
+
self._shared_extra_tools.clear()
|
|
142
|
+
self._shared_extra_tools.extend(tools)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def sandbox_root(self) -> Path:
|
|
146
|
+
"""总沙箱根目录。"""
|
|
147
|
+
return self._sandbox_root
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def harness_enabled(self) -> bool:
|
|
151
|
+
"""各会话是否向模型注册 Harness 沙箱工具。"""
|
|
152
|
+
return self._harness_enabled
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def current_time_tool_enabled(self) -> bool:
|
|
156
|
+
"""各会话是否向模型注册 ``builtin__current_time``。"""
|
|
157
|
+
return self._current_time_tool_enabled
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def thinking_enabled(self) -> bool:
|
|
161
|
+
"""主对话是否在请求中开启思考模式。"""
|
|
162
|
+
return self._thinking_enabled
|
|
163
|
+
|
|
164
|
+
def list_session_ids(self) -> tuple[str, ...]:
|
|
165
|
+
"""当前已打开(在内存中)的会话 id。"""
|
|
166
|
+
return tuple(sorted(self._sessions))
|
|
167
|
+
|
|
168
|
+
def has_session(self, session_id: str) -> bool:
|
|
169
|
+
"""是否已有该会话实例。"""
|
|
170
|
+
label = validate_session_id(session_id)
|
|
171
|
+
return label in self._sessions
|
|
172
|
+
|
|
173
|
+
def get_session(self, session_id: str) -> AgentSession | None:
|
|
174
|
+
"""
|
|
175
|
+
获取已打开的会话。
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
session_id: 会话 id
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
已存在则返回会话,否则 ``None``
|
|
182
|
+
"""
|
|
183
|
+
label = validate_session_id(session_id)
|
|
184
|
+
return self._sessions.get(label)
|
|
185
|
+
|
|
186
|
+
def open_session(
|
|
187
|
+
self,
|
|
188
|
+
session_id: str,
|
|
189
|
+
*,
|
|
190
|
+
memory: MemorySystem | None = None,
|
|
191
|
+
) -> AgentSession:
|
|
192
|
+
"""
|
|
193
|
+
打开或复用一会话:分配子沙箱并安装 Harness 与 Agent。
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
session_id: 会话 id;重复打开返回同一 ``AgentSession`` 实例
|
|
197
|
+
memory: 外部已构造的记忆系统;未传且 AgentApp 已配置记忆模型时自动创建
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
该会话实例
|
|
201
|
+
"""
|
|
202
|
+
label = validate_session_id(session_id)
|
|
203
|
+
existing = self._sessions.get(label)
|
|
204
|
+
if existing is not None:
|
|
205
|
+
return existing
|
|
206
|
+
session_root = session_workspace(self._sandbox_root, label)
|
|
207
|
+
harness_workspace = session_harness_workspace(session_root)
|
|
208
|
+
session_memory = memory
|
|
209
|
+
if session_memory is None:
|
|
210
|
+
session_memory = self._build_default_memory(session_root)
|
|
211
|
+
session = build_session(
|
|
212
|
+
session_id=label,
|
|
213
|
+
workspace=session_root,
|
|
214
|
+
harness_workspace=harness_workspace,
|
|
215
|
+
skill_roots=self._skill_roots,
|
|
216
|
+
rule_paths=self._rule_paths,
|
|
217
|
+
api_key=self._api_key,
|
|
218
|
+
model=self._model,
|
|
219
|
+
base_url=self._base_url,
|
|
220
|
+
temperature=self._temperature,
|
|
221
|
+
max_tokens=self._max_tokens,
|
|
222
|
+
thinking_enabled=self._thinking_enabled,
|
|
223
|
+
max_steps=self._max_steps,
|
|
224
|
+
extra_tools=self._shared_extra_tools,
|
|
225
|
+
listeners=self._listeners,
|
|
226
|
+
memory=session_memory,
|
|
227
|
+
harness_enabled=self._harness_enabled,
|
|
228
|
+
current_time_tool_enabled=self._current_time_tool_enabled,
|
|
229
|
+
)
|
|
230
|
+
self._sessions[label] = session
|
|
231
|
+
return session
|
|
232
|
+
|
|
233
|
+
async def run(self, packet: RunInputPacket) -> RunOutputPacket:
|
|
234
|
+
"""
|
|
235
|
+
运行入口:接收输入数据包,规划并逐步执行后返回输出数据包。
|
|
236
|
+
|
|
237
|
+
单次调用内打开会话、处理完毕后从内存移除会话实例;Harness 与对话状态落在
|
|
238
|
+
``sessions/<session_id>/``。运行时事件通过构造时传入的 ``listeners`` 回调。
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
packet: 用户名、会话 id、要求、附件路径与是否清空会话状态等
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
结构化回答与须交还用户的文件路径列表
|
|
245
|
+
"""
|
|
246
|
+
label = validate_session_id(packet.session_id)
|
|
247
|
+
user_name = packet.user_name.strip()
|
|
248
|
+
if not user_name:
|
|
249
|
+
raise ValueError("user_name 不能为空")
|
|
250
|
+
session_root = session_workspace(self._sandbox_root, label)
|
|
251
|
+
if packet.clear:
|
|
252
|
+
reset_session_subdirs(session_root)
|
|
253
|
+
clear_conversation(session_root)
|
|
254
|
+
self.close_session(label)
|
|
255
|
+
session = self.open_session(label)
|
|
256
|
+
try:
|
|
257
|
+
if not packet.clear and session.memory is None:
|
|
258
|
+
session.replace_messages(load_conversation(session_root))
|
|
259
|
+
if packet.input_files and not self._harness_enabled:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"已传入 input_files 但 AgentApp.harness_enabled 为 False;"
|
|
262
|
+
"请启用 Harness 或勿附带附件",
|
|
263
|
+
)
|
|
264
|
+
staged = stage_input_files(packet.input_files, session.harness.workspace)
|
|
265
|
+
file_context = format_input_files_context(staged)
|
|
266
|
+
user_message = _compose_plan_user_message(packet.request, file_context)
|
|
267
|
+
plan_result = await session.run_with_plan(
|
|
268
|
+
user_message=user_message,
|
|
269
|
+
speaker=user_name,
|
|
270
|
+
extra_planning_context=file_context,
|
|
271
|
+
)
|
|
272
|
+
answer, rel_files = parse_structured_run_output(plan_result.final_output)
|
|
273
|
+
output_files = resolve_output_files(rel_files, session.harness.workspace)
|
|
274
|
+
if session.memory is None:
|
|
275
|
+
save_conversation(session_root, list(session.messages))
|
|
276
|
+
packet = RunOutputPacket(
|
|
277
|
+
user_name=user_name,
|
|
278
|
+
session_id=label,
|
|
279
|
+
answer=answer,
|
|
280
|
+
output_files=output_files,
|
|
281
|
+
)
|
|
282
|
+
await notify_app_run_end(self._listeners, packet)
|
|
283
|
+
return packet
|
|
284
|
+
finally:
|
|
285
|
+
self.close_session(label)
|
|
286
|
+
|
|
287
|
+
def _build_default_memory(self, workspace: Path) -> MemorySystem | None:
|
|
288
|
+
key = self._memory_api_key
|
|
289
|
+
model = self._memory_model
|
|
290
|
+
base = self._memory_base_url
|
|
291
|
+
if not key or not model or not base:
|
|
292
|
+
return None
|
|
293
|
+
storage = workspace / SESSION_MEMORY_SUBDIR
|
|
294
|
+
return MemorySystem(
|
|
295
|
+
storage,
|
|
296
|
+
api_key=key,
|
|
297
|
+
model=model,
|
|
298
|
+
base_url=base,
|
|
299
|
+
config=self._memory_config,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def close_session(self, session_id: str) -> bool:
|
|
303
|
+
"""
|
|
304
|
+
从应用中移除会话实例(不删除磁盘上的子沙箱内容)。
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
session_id: 会话 id
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
若原先存在并已移除则为 True
|
|
311
|
+
"""
|
|
312
|
+
label = validate_session_id(session_id)
|
|
313
|
+
return self._sessions.pop(label, None) is not None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _compose_plan_user_message(request: str, file_context: str) -> str:
|
|
317
|
+
"""拼规划与记忆用的用户原文(不含最终交付 JSON 说明)。"""
|
|
318
|
+
parts: list[str] = [request.strip()]
|
|
319
|
+
if file_context.strip():
|
|
320
|
+
parts.append(file_context.strip())
|
|
321
|
+
return "\n\n".join(parts)
|