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,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from ai_agent.tools import Tool
|
|
6
|
+
|
|
7
|
+
ToolFactory = Callable[[], Tool]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BuiltinToolRegistry:
|
|
11
|
+
"""
|
|
12
|
+
宿主预注册的安全工具库,供技能 frontmatter 以内置引用方式绑定。
|
|
13
|
+
|
|
14
|
+
不允许从技能文件动态执行任意 Python;未注册名称在启用技能时无法解析。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._factories: dict[str, ToolFactory] = {}
|
|
19
|
+
|
|
20
|
+
def register(self, name: str, factory: ToolFactory) -> None:
|
|
21
|
+
"""
|
|
22
|
+
注册内置工具工厂。
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name: 不含 ``builtin:`` 前缀的名称
|
|
26
|
+
factory: 每次绑定 skill 时调用,返回新的 Tool 实例
|
|
27
|
+
"""
|
|
28
|
+
key = name.strip()
|
|
29
|
+
if not key:
|
|
30
|
+
raise ValueError("内置工具名不能为空")
|
|
31
|
+
self._factories[key] = factory
|
|
32
|
+
|
|
33
|
+
def resolve(self, handler: str) -> Tool | None:
|
|
34
|
+
"""
|
|
35
|
+
解析 handler 引用。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
handler: 形如 ``builtin:tool_name``
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
可注册的 Tool;未知 handler 时返回 None
|
|
42
|
+
"""
|
|
43
|
+
cleaned = handler.strip()
|
|
44
|
+
if not cleaned.startswith("builtin:"):
|
|
45
|
+
return None
|
|
46
|
+
name = cleaned[len("builtin:") :].strip()
|
|
47
|
+
if not name:
|
|
48
|
+
return None
|
|
49
|
+
factory = self._factories.get(name)
|
|
50
|
+
if factory is None:
|
|
51
|
+
return None
|
|
52
|
+
return factory()
|
|
53
|
+
|
|
54
|
+
def known_names(self) -> tuple[str, ...]:
|
|
55
|
+
"""已注册的内置工具名。"""
|
|
56
|
+
return tuple(sorted(self._factories))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ai_agent.skill.frontmatter import split_frontmatter
|
|
7
|
+
from ai_agent.skill.roots import SkillRootsSandbox
|
|
8
|
+
|
|
9
|
+
_MAX_READ_BYTES = 512 * 1024
|
|
10
|
+
_SKILL_FILE = "SKILL.md"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SkillSummary:
|
|
15
|
+
"""扫描得到的技能摘要(不含正文)。"""
|
|
16
|
+
|
|
17
|
+
root_key: str
|
|
18
|
+
"""配置的技能根键名。"""
|
|
19
|
+
skill_id: str
|
|
20
|
+
"""根目录下技能子文件夹名。"""
|
|
21
|
+
name: str
|
|
22
|
+
"""frontmatter 中的展示名。"""
|
|
23
|
+
description: str
|
|
24
|
+
"""frontmatter 中的简短说明。"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def skill_ref(self) -> str:
|
|
28
|
+
return f"{self.root_key}/{self.skill_id}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def scan_skills(sandbox: SkillRootsSandbox) -> list[SkillSummary]:
|
|
32
|
+
"""
|
|
33
|
+
扫描所有根目录下含 SKILL.md 的子目录。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
sandbox: 已配置的 skill 根
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
按 skill_ref 字典序排列的摘要列表
|
|
40
|
+
"""
|
|
41
|
+
found: list[SkillSummary] = []
|
|
42
|
+
for root_key in sandbox.root_keys:
|
|
43
|
+
root = sandbox.root_path(root_key)
|
|
44
|
+
if not root.is_dir():
|
|
45
|
+
continue
|
|
46
|
+
for child in sorted(root.iterdir()):
|
|
47
|
+
if not child.is_dir():
|
|
48
|
+
continue
|
|
49
|
+
skill_md = child / _SKILL_FILE
|
|
50
|
+
if not skill_md.is_file():
|
|
51
|
+
continue
|
|
52
|
+
summary = _summary_from_file(root_key, child.name, skill_md)
|
|
53
|
+
found.append(summary)
|
|
54
|
+
found.sort(key=lambda item: item.skill_ref)
|
|
55
|
+
return found
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_skill_text(sandbox: SkillRootsSandbox, skill_ref: str) -> str:
|
|
59
|
+
"""
|
|
60
|
+
读取 SKILL.md 全文。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
sandbox: skill 根沙箱
|
|
64
|
+
skill_ref: ``{root_key}/{skill_id}``
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
文件全文
|
|
68
|
+
"""
|
|
69
|
+
path = sandbox.skill_md_path(skill_ref)
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
raise ValueError(f"未找到 SKILL.md: {skill_ref}")
|
|
72
|
+
return _read_bounded(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_skill_list(summaries: list[SkillSummary]) -> str:
|
|
76
|
+
"""将摘要列表格式化为模型可读文本(不含宿主机绝对路径)。"""
|
|
77
|
+
if not summaries:
|
|
78
|
+
return "(未找到 skill)"
|
|
79
|
+
lines: list[str] = []
|
|
80
|
+
for item in summaries:
|
|
81
|
+
lines.append(
|
|
82
|
+
f"- {item.skill_ref} | name={item.name} | {item.description}"
|
|
83
|
+
)
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _summary_from_file(root_key: str, skill_id: str, skill_md: Path) -> SkillSummary:
|
|
88
|
+
raw = _read_bounded(skill_md)
|
|
89
|
+
meta, _ = split_frontmatter(raw)
|
|
90
|
+
name = meta.get("name", "").strip() or skill_id
|
|
91
|
+
description = meta.get("description", "").strip()
|
|
92
|
+
return SkillSummary(
|
|
93
|
+
root_key=root_key,
|
|
94
|
+
skill_id=skill_id,
|
|
95
|
+
name=name,
|
|
96
|
+
description=description,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_bounded(path: Path) -> str:
|
|
101
|
+
size = path.stat().st_size
|
|
102
|
+
if size > _MAX_READ_BYTES:
|
|
103
|
+
raise ValueError(f"文件过大(>{_MAX_READ_BYTES} 字节)")
|
|
104
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_FRONTMATTER_RE = re.compile(
|
|
6
|
+
r"\A---\r?\n(.*?)\r?\n---\r?\n?",
|
|
7
|
+
re.DOTALL,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def split_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
12
|
+
"""
|
|
13
|
+
拆分 YAML frontmatter 与正文。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
text: 完整文件内容
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
元数据键值对与正文(无 frontmatter 时元数据为空 dict)
|
|
20
|
+
"""
|
|
21
|
+
match = _FRONTMATTER_RE.match(text)
|
|
22
|
+
if not match:
|
|
23
|
+
return {}, text
|
|
24
|
+
meta = _parse_simple_yaml(match.group(1))
|
|
25
|
+
body = text[match.end() :]
|
|
26
|
+
return meta, body
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compose_skill_md(meta: dict[str, str], body: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
组装带 frontmatter 的 SKILL.md 文本。
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
meta: 元数据;空 dict 时不写 frontmatter
|
|
35
|
+
body: Markdown 正文
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
完整文件内容
|
|
39
|
+
"""
|
|
40
|
+
normalized_body = body
|
|
41
|
+
if normalized_body and not normalized_body.endswith("\n"):
|
|
42
|
+
normalized_body += "\n"
|
|
43
|
+
if not meta:
|
|
44
|
+
return normalized_body
|
|
45
|
+
lines = ["---"]
|
|
46
|
+
for key in sorted(meta.keys()):
|
|
47
|
+
lines.append(f"{key}: {_yaml_scalar(meta[key])}")
|
|
48
|
+
lines.append("---")
|
|
49
|
+
lines.append("")
|
|
50
|
+
if normalized_body:
|
|
51
|
+
return "\n".join(lines) + "\n" + normalized_body.lstrip("\n")
|
|
52
|
+
return "\n".join(lines) + "\n"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_simple_yaml(block: str) -> dict[str, str]:
|
|
56
|
+
meta: dict[str, str] = {}
|
|
57
|
+
for line in block.splitlines():
|
|
58
|
+
stripped = line.strip()
|
|
59
|
+
if not stripped or stripped.startswith("#"):
|
|
60
|
+
continue
|
|
61
|
+
if ":" not in stripped:
|
|
62
|
+
continue
|
|
63
|
+
key, _, value = stripped.partition(":")
|
|
64
|
+
key = key.strip()
|
|
65
|
+
value = value.strip()
|
|
66
|
+
if value.startswith('"') and value.endswith('"') and len(value) >= 2:
|
|
67
|
+
value = value[1:-1]
|
|
68
|
+
elif value.startswith("'") and value.endswith("'") and len(value) >= 2:
|
|
69
|
+
value = value[1:-1]
|
|
70
|
+
if key:
|
|
71
|
+
meta[key] = value
|
|
72
|
+
return meta
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _yaml_scalar(value: str) -> str:
|
|
76
|
+
if not value:
|
|
77
|
+
return '""'
|
|
78
|
+
if re.search(r"[\n:#\"'\\]", value):
|
|
79
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
80
|
+
return f'"{escaped}"'
|
|
81
|
+
if re.search(r"[^\w.\-/]", value):
|
|
82
|
+
return f'"{value}"'
|
|
83
|
+
return value
|