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/loop.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import AsyncIterator
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from ai_agent.context import AgentContext, RunContext, RunStatus, ToolInvocation
|
|
8
|
+
from ai_agent.listener import (
|
|
9
|
+
AgentListener,
|
|
10
|
+
notify_output_delta,
|
|
11
|
+
notify_run_end,
|
|
12
|
+
notify_run_start,
|
|
13
|
+
notify_thinking_delta,
|
|
14
|
+
notify_tool_end,
|
|
15
|
+
notify_tool_start,
|
|
16
|
+
)
|
|
17
|
+
from ai_agent.llm import StreamChunk, StreamKind
|
|
18
|
+
from ai_agent.react_tool_turn import deferred_tool_reply, should_defer_tool_in_batch
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReactLoop:
|
|
22
|
+
"""多步 ReAct 运行;更新 RunContext 中的思考、工具实例与最终 output。"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, context: AgentContext) -> None:
|
|
25
|
+
self._context = context
|
|
26
|
+
|
|
27
|
+
async def run(self, run: RunContext) -> AsyncIterator[RunContext]:
|
|
28
|
+
"""
|
|
29
|
+
驱动单轮上下文直至完成、失败或超步数;每有更新 yield 同一对象。
|
|
30
|
+
"""
|
|
31
|
+
run.status = RunStatus.RUNNING
|
|
32
|
+
run.thinking = ""
|
|
33
|
+
run.output = ""
|
|
34
|
+
await notify_run_start(self._context.listeners, run)
|
|
35
|
+
yield run
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
for _ in range(self._context.max_steps):
|
|
39
|
+
run.thinking = ""
|
|
40
|
+
pending_calls: list[ToolInvocation] = []
|
|
41
|
+
api_tools = self._context.tools.api_tools() or None
|
|
42
|
+
|
|
43
|
+
async for chunk in self._context.llm.stream(run, tools=api_tools):
|
|
44
|
+
self._apply_chunk(run, chunk, pending_calls)
|
|
45
|
+
await self._notify_chunk(self._context.listeners, chunk, run)
|
|
46
|
+
yield run
|
|
47
|
+
if chunk.kind == StreamKind.DONE:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if pending_calls:
|
|
51
|
+
turn: list[ToolInvocation] = []
|
|
52
|
+
for inv in pending_calls:
|
|
53
|
+
run.tool_invocations.append(inv)
|
|
54
|
+
turn.append(inv)
|
|
55
|
+
yield run
|
|
56
|
+
await notify_tool_start(self._context.listeners, inv, run)
|
|
57
|
+
if should_defer_tool_in_batch(inv, pending_calls):
|
|
58
|
+
inv.answer = deferred_tool_reply()
|
|
59
|
+
inv.ok = True
|
|
60
|
+
else:
|
|
61
|
+
await self._context.tools.execute(inv)
|
|
62
|
+
await notify_tool_end(self._context.listeners, inv, run)
|
|
63
|
+
yield run
|
|
64
|
+
run.tool_turns.append(turn)
|
|
65
|
+
run.output = ""
|
|
66
|
+
continue
|
|
67
|
+
if run.output:
|
|
68
|
+
run.status = RunStatus.COMPLETED
|
|
69
|
+
yield run
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if run.thinking and not run.output:
|
|
73
|
+
run.output = run.thinking
|
|
74
|
+
run.status = RunStatus.COMPLETED
|
|
75
|
+
yield run
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
run.status = RunStatus.MAX_STEPS
|
|
79
|
+
yield run
|
|
80
|
+
|
|
81
|
+
except Exception: # noqa: BLE001 — 运行边界
|
|
82
|
+
run.status = RunStatus.FAILED
|
|
83
|
+
yield run
|
|
84
|
+
raise
|
|
85
|
+
finally:
|
|
86
|
+
await notify_run_end(self._context.listeners, run)
|
|
87
|
+
|
|
88
|
+
async def _notify_chunk(
|
|
89
|
+
self,
|
|
90
|
+
listeners: Sequence[AgentListener],
|
|
91
|
+
chunk: StreamChunk,
|
|
92
|
+
run: RunContext,
|
|
93
|
+
) -> None:
|
|
94
|
+
if not chunk.delta:
|
|
95
|
+
return
|
|
96
|
+
if chunk.kind == StreamKind.REASONING:
|
|
97
|
+
await notify_thinking_delta(listeners, chunk.delta, run)
|
|
98
|
+
elif chunk.kind == StreamKind.TEXT:
|
|
99
|
+
await notify_output_delta(listeners, chunk.delta, run)
|
|
100
|
+
|
|
101
|
+
def _apply_chunk(
|
|
102
|
+
self,
|
|
103
|
+
run: RunContext,
|
|
104
|
+
chunk: StreamChunk,
|
|
105
|
+
pending_calls: list[ToolInvocation],
|
|
106
|
+
) -> None:
|
|
107
|
+
if chunk.kind == StreamKind.REASONING:
|
|
108
|
+
if chunk.delta:
|
|
109
|
+
run.thinking += chunk.delta
|
|
110
|
+
if pending_calls:
|
|
111
|
+
pending_calls[-1].thinking += chunk.delta
|
|
112
|
+
elif chunk.kind == StreamKind.TEXT:
|
|
113
|
+
if chunk.delta:
|
|
114
|
+
run.output += chunk.delta
|
|
115
|
+
elif chunk.kind == StreamKind.TOOL_CALL:
|
|
116
|
+
if chunk.tool_call_id and chunk.tool_name is not None:
|
|
117
|
+
inv = ToolInvocation(
|
|
118
|
+
call_id=chunk.tool_call_id,
|
|
119
|
+
tool_name=chunk.tool_name,
|
|
120
|
+
arguments=dict(chunk.tool_arguments or {}),
|
|
121
|
+
thinking=run.thinking,
|
|
122
|
+
)
|
|
123
|
+
pending_calls.append(inv)
|
|
124
|
+
run.thinking = ""
|
ai_agent/mcp_config.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class McpStdioServerConfig(BaseModel):
|
|
10
|
+
"""单个 MCP stdio 子进程启动项。"""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="forbid")
|
|
13
|
+
|
|
14
|
+
command: str = Field(description="启动 MCP server 的可执行文件路径或命令名")
|
|
15
|
+
args: list[str] = Field(default_factory=list, description="传给 command 的参数列表")
|
|
16
|
+
env: dict[str, str] | None = Field(
|
|
17
|
+
default=None,
|
|
18
|
+
description="传入子进程的环境变量;与 MCP SDK 默认项合并,不继承宿主进程 os.environ",
|
|
19
|
+
)
|
|
20
|
+
cwd: str | None = Field(default=None, description="子进程工作目录;省略则使用当前工作目录")
|
|
21
|
+
|
|
22
|
+
@field_validator("command")
|
|
23
|
+
@classmethod
|
|
24
|
+
def _command_non_empty(cls, value: str) -> str:
|
|
25
|
+
stripped = value.strip()
|
|
26
|
+
if not stripped:
|
|
27
|
+
raise ValueError("command 不能为空")
|
|
28
|
+
return stripped
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class McpConfig(BaseModel):
|
|
32
|
+
"""MCP 工具加载配置,与常见 ``mcpServers`` JSON 结构一致。"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
35
|
+
|
|
36
|
+
mcp_servers: dict[str, McpStdioServerConfig] = Field(
|
|
37
|
+
alias="mcpServers",
|
|
38
|
+
description="按名称索引的 MCP server 启动配置",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_mcp_config(data: McpConfig | Mapping[str, Any]) -> McpConfig:
|
|
43
|
+
"""
|
|
44
|
+
将映射或已有模型规范为 ``McpConfig``。
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data: 含 ``mcpServers`` 键的映射,或已校验的 ``McpConfig``
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
校验后的配置模型
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(data, McpConfig):
|
|
53
|
+
return data
|
|
54
|
+
return McpConfig.model_validate(dict(data))
|
ai_agent/mcp_loader.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from contextlib import AsyncExitStack
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mcp import ClientSession, StdioServerParameters
|
|
8
|
+
from mcp.client.stdio import get_default_environment, stdio_client
|
|
9
|
+
|
|
10
|
+
from ai_agent.mcp_config import McpConfig, McpStdioServerConfig, parse_mcp_config
|
|
11
|
+
from ai_agent.tools import Tool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _stdio_env(server_env: dict[str, str] | None) -> dict[str, str]:
|
|
15
|
+
"""
|
|
16
|
+
组装 MCP 子进程环境变量。
|
|
17
|
+
|
|
18
|
+
仅合并 MCP SDK 默认项与 ``mcp.json`` 中该 server 的 ``env``,
|
|
19
|
+
不继承当前进程的 ``os.environ``,避免与示例 ``.env`` 中的 LLM 等变量串扰。
|
|
20
|
+
"""
|
|
21
|
+
merged = dict(get_default_environment())
|
|
22
|
+
if server_env:
|
|
23
|
+
merged.update(server_env)
|
|
24
|
+
return merged
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MCPToolLoader:
|
|
28
|
+
"""
|
|
29
|
+
按已校验的 MCP 配置启动 stdio 服务并保持会话,将远程工具包装为库内 ``Tool``。
|
|
30
|
+
|
|
31
|
+
会话在 ``close()`` 之前须保持存活,以便后续 ReAct 循环通过同一连接调用工具。
|
|
32
|
+
配置文件读取与多格式解析由调用方负责,本类只接收 ``McpConfig`` 或等价映射。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._stack = AsyncExitStack()
|
|
37
|
+
|
|
38
|
+
async def load(self, config: McpConfig | Mapping[str, Any]) -> list[Tool]:
|
|
39
|
+
"""
|
|
40
|
+
按配置启动全部 MCP server 并收集工具。
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: ``McpConfig`` 或含 ``mcpServers`` 的映射(经 Pydantic 校验)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
各 server 暴露的工具列表
|
|
47
|
+
"""
|
|
48
|
+
parsed = parse_mcp_config(config)
|
|
49
|
+
tools: list[Tool] = []
|
|
50
|
+
for server_name, server_config in parsed.mcp_servers.items():
|
|
51
|
+
server_tools = await self._load_server(server_name, server_config)
|
|
52
|
+
tools.extend(server_tools)
|
|
53
|
+
return tools
|
|
54
|
+
|
|
55
|
+
async def _load_server(
|
|
56
|
+
self,
|
|
57
|
+
server_name: str,
|
|
58
|
+
server_config: McpStdioServerConfig,
|
|
59
|
+
) -> list[Tool]:
|
|
60
|
+
params = StdioServerParameters(
|
|
61
|
+
command=server_config.command,
|
|
62
|
+
args=server_config.args,
|
|
63
|
+
env=_stdio_env(server_config.env),
|
|
64
|
+
cwd=server_config.cwd,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
read, write = await self._stack.enter_async_context(stdio_client(params))
|
|
68
|
+
session = await self._stack.enter_async_context(ClientSession(read, write))
|
|
69
|
+
await session.initialize()
|
|
70
|
+
|
|
71
|
+
result = await session.list_tools()
|
|
72
|
+
tools: list[Tool] = []
|
|
73
|
+
|
|
74
|
+
for mcp_tool in result.tools:
|
|
75
|
+
ai_tool_name = f"{server_name}__{mcp_tool.name}"
|
|
76
|
+
handler = _make_tool_handler(session, mcp_tool.name)
|
|
77
|
+
tools.append(
|
|
78
|
+
Tool(
|
|
79
|
+
name=ai_tool_name,
|
|
80
|
+
description=mcp_tool.description or "",
|
|
81
|
+
parameters=mcp_tool.inputSchema,
|
|
82
|
+
handler=handler,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return tools
|
|
87
|
+
|
|
88
|
+
async def close(self) -> None:
|
|
89
|
+
"""关闭已启动的 MCP 会话与 stdio 子进程。"""
|
|
90
|
+
await self._stack.aclose()
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _result_to_text(result: Any) -> str:
|
|
94
|
+
parts: list[str] = []
|
|
95
|
+
for item in getattr(result, "content", []):
|
|
96
|
+
text = getattr(item, "text", None)
|
|
97
|
+
if text is not None:
|
|
98
|
+
parts.append(text)
|
|
99
|
+
return "\n".join(parts)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _make_tool_handler(
|
|
103
|
+
session: ClientSession,
|
|
104
|
+
mcp_tool_name: str,
|
|
105
|
+
) -> Any:
|
|
106
|
+
async def handler(**arguments: Any) -> str:
|
|
107
|
+
call_result = await session.call_tool(mcp_tool_name, arguments)
|
|
108
|
+
return MCPToolLoader._result_to_text(call_result)
|
|
109
|
+
|
|
110
|
+
return handler
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from ai_agent.memory.models import (
|
|
7
|
+
DateMemoryEntry,
|
|
8
|
+
DateMemoryDay,
|
|
9
|
+
ImportantMemoryEntry,
|
|
10
|
+
LongTermChunk,
|
|
11
|
+
MemoryMessage,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ShortToDateWork:
|
|
17
|
+
batch: tuple[MemoryMessage, ...]
|
|
18
|
+
batch_size: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ShortToDateResult:
|
|
23
|
+
entries: tuple[DateMemoryEntry, ...]
|
|
24
|
+
important_texts: tuple[str, ...]
|
|
25
|
+
day_label: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class DateToLongWork:
|
|
30
|
+
day_label: str
|
|
31
|
+
entries: tuple[DateMemoryEntry, ...]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class DateToLongResult:
|
|
36
|
+
chunk: LongTermChunk
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class CompressLongWork:
|
|
41
|
+
chunks: tuple[LongTermChunk, ...]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class CompressLongResult:
|
|
46
|
+
chunks: tuple[LongTermChunk, ...]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class CompressImportantWork:
|
|
51
|
+
entries: tuple[ImportantMemoryEntry, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class CompressImportantResult:
|
|
56
|
+
entries: tuple[ImportantMemoryEntry, ...]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
CompressionWork = Union[
|
|
60
|
+
ShortToDateWork,
|
|
61
|
+
DateToLongWork,
|
|
62
|
+
CompressLongWork,
|
|
63
|
+
CompressImportantWork,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
CompressionResult = Union[
|
|
67
|
+
ShortToDateResult,
|
|
68
|
+
DateToLongResult,
|
|
69
|
+
CompressLongResult,
|
|
70
|
+
CompressImportantResult,
|
|
71
|
+
]
|