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.
Files changed (62) hide show
  1. ai_agent/__init__.py +66 -0
  2. ai_agent/agent.py +122 -0
  3. ai_agent/app/__init__.py +10 -0
  4. ai_agent/app/_workspace.py +127 -0
  5. ai_agent/app/app.py +321 -0
  6. ai_agent/app/harness_io.py +109 -0
  7. ai_agent/app/output_format.py +77 -0
  8. ai_agent/app/packet.py +39 -0
  9. ai_agent/app/session.py +742 -0
  10. ai_agent/app/session_store.py +85 -0
  11. ai_agent/builtin_tools/__init__.py +18 -0
  12. ai_agent/builtin_tools/current_time.py +39 -0
  13. ai_agent/builtin_tools/pack.py +20 -0
  14. ai_agent/builtin_tools/prefix.py +11 -0
  15. ai_agent/context.py +151 -0
  16. ai_agent/harness/__init__.py +3 -0
  17. ai_agent/harness/current_time.py +25 -0
  18. ai_agent/harness/harness.py +324 -0
  19. ai_agent/harness/process.py +115 -0
  20. ai_agent/harness/prompts.py +38 -0
  21. ai_agent/harness/sandbox.py +139 -0
  22. ai_agent/json_extract.py +70 -0
  23. ai_agent/listener.py +172 -0
  24. ai_agent/llm.py +39 -0
  25. ai_agent/llm_openai.py +117 -0
  26. ai_agent/loop.py +124 -0
  27. ai_agent/mcp_config.py +54 -0
  28. ai_agent/mcp_loader.py +110 -0
  29. ai_agent/memory/__init__.py +9 -0
  30. ai_agent/memory/compression_work.py +71 -0
  31. ai_agent/memory/compressor.py +339 -0
  32. ai_agent/memory/config.py +40 -0
  33. ai_agent/memory/context_builder.py +57 -0
  34. ai_agent/memory/memory_system.py +561 -0
  35. ai_agent/memory/models.py +76 -0
  36. ai_agent/memory/snapshot_merge.py +158 -0
  37. ai_agent/memory/store.py +107 -0
  38. ai_agent/memory/worker.py +227 -0
  39. ai_agent/plan/__init__.py +15 -0
  40. ai_agent/plan/complete.py +64 -0
  41. ai_agent/plan/delivery.py +41 -0
  42. ai_agent/plan/display.py +46 -0
  43. ai_agent/plan/models.py +44 -0
  44. ai_agent/plan/parse.py +39 -0
  45. ai_agent/plan/planner.py +204 -0
  46. ai_agent/plan/runner.py +281 -0
  47. ai_agent/react_tool_turn.py +39 -0
  48. ai_agent/rule/__init__.py +3 -0
  49. ai_agent/rule/rules.py +36 -0
  50. ai_agent/skill/__init__.py +5 -0
  51. ai_agent/skill/builtin_registry.py +56 -0
  52. ai_agent/skill/catalog.py +104 -0
  53. ai_agent/skill/frontmatter.py +83 -0
  54. ai_agent/skill/manager.py +486 -0
  55. ai_agent/skill/models.py +31 -0
  56. ai_agent/skill/roots.py +150 -0
  57. ai_agent/skill/skill_kit.py +80 -0
  58. ai_agent/skill/tool_declarations.py +68 -0
  59. ai_agent/tools.py +123 -0
  60. python_library_ai_agent-0.1.0.dist-info/METADATA +10 -0
  61. python_library_ai_agent-0.1.0.dist-info/RECORD +62 -0
  62. 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