init repo

This commit is contained in:
Summer Shen
2026-03-31 09:14:32 +08:00
commit 9578366e36
34 changed files with 5200 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-30
@@ -0,0 +1,147 @@
## Context
本项目是一个全新的 Python 项目(greenfield),目标是构建一个基于 LlamaIndex Workflow 的 Agent,能够动态加载和执行 SKILL。
**当前状态:**
- 项目仅有 `src/llama_agent_skills/__init__.py` 中一个空的 `main()` 入口
- 无任何现有依赖(`pyproject.toml``dependencies = []`
- `.opencode/skills/``.github/skills/` 下有 4 个 SKILL.md 示例文件,定义了 SKILL 的标准格式
**SKILL 格式(已有约定):**
- YAML frontmatter`name``description``license``metadata`author、version
- Markdown 正文:自由格式的指令文本,包含 Steps、Guardrails、Input/Output 等段落
**LlamaIndex Workflow 关键能力(调研结论):**
- `Workflow` 基类:事件驱动、`@step` 装饰器、`Context` 状态管理
- `AgentWorkflow.from_tools_or_functions()`:接受 `tools` 列表和 `system_prompt`
- `FunctionAgent`:支持 `system_prompt``tools``can_handoff_to`
- 工具列表存储为实例变量 `self.tools`,可运行时动态增删
- `Context.set/get` 提供步骤间状态共享
**约束:**
- 不引入 Shell/subprocess,纯 Python 运行
- 脚本类 SKILL 功能暂不支持
- 需要支持线上部署场景
## Goals / Non-Goals
**Goals:**
- 实现 SKILL.md 文件的解析器,能提取 frontmatter 元数据和正文指令
- 实现 SKILL 注册表,支持从目录扫描、运行时动态注册/注销
- 构建基于 LlamaIndex Workflow 的核心 Agent,将 SKILL 作为 system prompt 的扩展注入
- 提供清晰的 Python API,方便程序化创建和运行 Agent
- 保持架构简洁,为后续扩展(MCP 工具、多 Agent 协作)留出空间
**Non-Goals:**
- 不实现 Shell/subprocess 执行能力(安全约束)
- 不实现 SKILL 内定义的脚本/命令执行功能
- 不实现多 Agent 协作/handoff(后续迭代)
- 不实现 SKILL 的远程加载(仅支持本地文件系统)
- 不实现 Web UI 或 REST API 层
- 不实现 MCP server/client 集成(后续迭代)
## Decisions
### D1: SKILL 注入方式 — System Prompt 拼接
**决定:** 将 SKILL 的 Markdown 正文作为 system prompt 的一部分注入 Agent。
**备选方案:**
- A) 将 SKILL 转换为 LlamaIndex `BaseTool`(每个 SKILL 变成一个可调用工具)
- B) 将 SKILL 注入 system prompt(当前选择)
- C) 将 SKILL 作为 RAG 文档,通过检索注入
**选择 B 的理由:**
- SKILL 本质是「行为指令」而非「可调用函数」,它定义了 Agent 应该如何行动,而不是提供一个新的能力入口
- System prompt 注入最自然,与 LlamaIndex 的 `system_prompt` 参数直接对应
- 方案 A 需要为每个 SKILL 包装一个工具入口,增加不必要的间接层
- 方案 C 引入 RAG 复杂度,对于有限数量的 SKILL 是过度设计
- 后续如果 SKILL 需要携带工具定义,可以在 SKILL 格式中扩展 `tools` 字段,注册为真正的工具
### D2: SKILL.md 解析策略 — python-frontmatter 库
**决定:** 使用 `python-frontmatter` 库解析 SKILL.md 的 YAML frontmatter + Markdown body。
**备选方案:**
- A) 手写 YAML + Markdown 分割解析器
- B) 使用 `python-frontmatter` 库(当前选择)
- C) 使用 `pyyaml` + 手动分割 `---` 分隔符
**选择 B 的理由:**
- `python-frontmatter` 是成熟的库,专门处理 frontmatter 格式,边界情况覆盖好
- 减少自研代码量和 bug 风险
- 自动处理 YAML 类型转换
### D3: Agent 架构 — 基于 AgentWorkflow 的单 Agent
**决定:** 使用 `AgentWorkflow.from_tools_or_functions()` 构建单 Agent,通过 `system_prompt` 参数注入 SKILL 上下文。
**备选方案:**
- A) 自定义 `Workflow` 子类,手动实现所有步骤
- B) 使用 `AgentWorkflow.from_tools_or_functions()`(当前选择)
- C) 使用 `FunctionAgent` + 多 Agent 编排
**选择 B 的理由:**
- `AgentWorkflow` 提供开箱即用的对话循环、工具调用、流式输出
- 对于第一版,单 Agent 足够验证 SKILL 注入概念
- 如需自定义步骤(如 SKILL 热加载),可以后续继承扩展
- 方案 A 需要重写大量 LlamaIndex 已有逻辑
- 方案 C 在当前阶段是过度设计
### D4: 模块结构 — 职责分离
**决定:** 按职责划分为以下模块:
```
src/llama_agent_skills/
├── __init__.py # 包入口 + main()
├── skill.py # Skill 数据模型 + SKILL.md 解析器
├── registry.py # SkillRegistry(扫描、注册、查询)
├── agent.py # Agent 构建与运行(封装 AgentWorkflow
└── config.py # 配置管理(环境变量、默认值)
```
**理由:**
- `skill.py` 专注数据解析,与 LlamaIndex 无关,可独立测试
- `registry.py` 管理 SKILL 生命周期,依赖 `skill.py` 但不依赖 `agent.py`
- `agent.py` 是唯一依赖 LlamaIndex 的模块,负责将 registry 中的 SKILL 组装为 Agent
- `config.py` 集中管理配置,避免硬编码散落
### D5: SKILL 上下文组装策略
**决定:** 在 Agent 启动时,将所有已注册 SKILL 的正文按固定格式拼接到 system prompt
```
{base_system_prompt}
---
## Active Skills
### Skill: {skill.name}
{skill.description}
{skill.body}
---
### Skill: {skill2.name}
...
```
**理由:**
- 结构化的分隔让 LLM 清楚区分不同 SKILL 的指令
- 每个 SKILL 带有名称和描述作为标题,便于 LLM 理解上下文切换
-`---` 分隔避免 SKILL 间指令混淆
## Risks / Trade-offs
- **[System prompt 长度爆炸]** → 当加载多个大型 SKILL 时,system prompt 可能超出 LLM context window。**缓解:** 限制同时加载的 SKILL 数量,后续可引入选择性加载或摘要机制。
- **[SKILL 间指令冲突]** → 多个 SKILL 可能包含矛盾指令。**缓解:** 第一版不解决,通过文档约定 SKILL 应自包含;后续可增加冲突检测。
- **[LlamaIndex 版本锁定]** → 依赖 `llama-index-core` 的特定 API(如 `AgentWorkflow`),升级可能 break。**缓解:** 通过 `agent.py` 隔离 LlamaIndex 依赖,仅在该模块中引用 LlamaIndex API。
- **[无 Shell 限制降低灵活性]** → 部分 SKILL 可能期望执行 CLI 命令(如 `openspec` CLI)。**缓解:** 明确标记为 Non-Goal,后续可通过安全沙箱或 API wrapper 支持。
- **[SKILL.md 格式无 schema 校验]** → frontmatter 字段错误不会被提前发现。**缓解:** 在 `skill.py` 中做基本校验(必填字段检查),后续可引入 JSON Schema 或 Pydantic 校验。
@@ -0,0 +1,31 @@
## Why
本项目需要构建一个基于 LlamaIndex Workflow 的 Agent,使其能够动态加载和执行 SKILL(以 SKILL.md 文件定义的结构化指令集)。SKILL 是一种将领域知识、工作流程和约束规则打包为可复用模块的机制,当前项目仅有空壳入口,需要从零实现 Agent 核心和 SKILL 注入能力。为了方便线上部署,设计上避免引入 Shell/子进程调用,脚本类功能暂不支持。
## What Changes
- 引入 LlamaIndex Workflow 作为 Agent 运行时框架(新依赖:`llama-index-core``llama-index-llms-openai` 等)
- 实现 SKILL 加载器:解析 SKILL.md 文件的 YAML frontmattername、description、metadata)和 Markdown 正文(instructions、steps、guardrails
- 实现 SKILL 注入机制:将 SKILL 内容动态注入 Agent 的 system prompt,作为行为指令扩展
- 构建核心 Agent Workflow:包含 LLM 对话、工具调用、SKILL 上下文管理等步骤
- 提供 SKILL 目录扫描与注册 API,支持运行时动态加载/卸载 SKILL
- 暂不支持 Shell/脚本执行类工具,保证纯 Python 运行环境安全
## Capabilities
### New Capabilities
- `skill-loader`: SKILL.md 文件的解析、验证与加载,支持 YAML frontmatter + Markdown body 的标准格式
- `agent-workflow`: 基于 LlamaIndex Workflow 的核心 Agent 实现,包含对话管理、工具调用、SKILL 上下文注入
- `skill-registry`: SKILL 的注册、发现与生命周期管理,支持运行时动态增删
### Modified Capabilities
_无现有 capability 需要修改。_
## Impact
- **代码**: 在 `src/llama_agent_skills/` 下新增多个模块(skill loader、agent workflow、skill registry
- **依赖**: 新增 `llama-index-core``llama-index-llms-openai`(或其他 LLM provider)、`pyyaml`(用于解析 frontmatter
- **入口**: 改造 `__init__.py``main()` 函数,提供 Agent 启动入口
- **配置**: 可能新增配置文件或环境变量支持(LLM API key、SKILL 目录路径等)
- **限制**: 不引入 `subprocess``os.system` 等 Shell 调用,确保线上安全运行
@@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: Agent 构建与 SKILL 注入
系统 SHALL 提供一个 Agent 构建函数/类,接受 SKILL 列表和 LLM 配置,创建一个集成了 SKILL 上下文的 LlamaIndex AgentWorkflow。
#### Scenario: 使用 SKILL 列表创建 Agent
- **WHEN** 调用 Agent 构建函数,传入一个包含 2 个 Skill 实例的列表和 LLM 配置
- **THEN** 创建的 AgentWorkflow 的 system prompt 中包含这 2 个 Skill 的 `name``description``body` 内容
#### Scenario: 无 SKILL 创建 Agent
- **WHEN** 调用 Agent 构建函数,传入空的 SKILL 列表
- **THEN** 创建的 AgentWorkflow 仅包含基础 system prompt,不含任何 SKILL 上下文
### Requirement: System Prompt 组装
系统 SHALL 按照固定格式将多个 SKILL 的内容组装到 system prompt 中,每个 SKILL 之间用分隔符隔开。
#### Scenario: 多 SKILL 的 prompt 格式
- **WHEN** 注入 2 个名为 "skill-a" 和 "skill-b" 的 SKILL
- **THEN** 生成的 system prompt 依次包含基础 prompt、"## Active Skills" 标题、每个 SKILL 的 "### Skill: {name}" 标题及其 body 内容,SKILL 之间用 `---` 分隔
#### Scenario: SKILL 顺序保持一致
- **WHEN** 按 ["skill-b", "skill-a"] 顺序注入 SKILL
- **THEN** system prompt 中 skill-b 的内容出现在 skill-a 之前
### Requirement: Agent 对话执行
系统 SHALL 提供运行 Agent 的能力,接受用户输入并返回 LLM 响应。
#### Scenario: 单轮对话
- **WHEN** 向 Agent 发送一条用户消息
- **THEN** Agent 调用 LLM 并返回文本响应
#### Scenario: 带工具的对话
- **WHEN** Agent 配置了工具函数,且用户消息触发了工具调用
- **THEN** Agent 执行工具调用并将结果整合到最终响应中
### Requirement: 工具注册
系统 SHALL 支持在创建 Agent 时注册 Python 可调用函数作为工具。
#### Scenario: 注册 Python 函数为工具
- **WHEN** 传入一个带 docstring 的 Python 函数到 Agent 构建函数的 `tools` 参数
- **THEN** 该函数被注册为 LlamaIndex 工具,Agent 可在对话中调用它
#### Scenario: 无工具的 Agent
- **WHEN** 创建 Agent 时不传入任何工具
- **THEN** Agent 正常运行,仅进行对话,不进行工具调用
@@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: SKILL.md 文件解析
系统 SHALL 能够解析符合标准格式的 SKILL.md 文件,提取 YAML frontmatter 中的元数据和 Markdown 正文中的指令内容。
#### Scenario: 解析标准 SKILL.md 文件
- **WHEN** 提供一个包含 YAML frontmatter`---` 分隔)和 Markdown 正文的 SKILL.md 文件路径
- **THEN** 系统返回一个 Skill 对象,包含 `name`str)、`description`str)、`metadata`dict)和 `body`str)字段
#### Scenario: 解析缺少必填字段的 SKILL.md
- **WHEN** 提供的 SKILL.md 文件的 frontmatter 中缺少 `name` 字段
- **THEN** 系统抛出 `SkillLoadError` 异常,包含明确的错误描述
#### Scenario: 解析无 frontmatter 的文件
- **WHEN** 提供的文件不包含 YAML frontmatter(无 `---` 分隔符)
- **THEN** 系统抛出 `SkillLoadError` 异常,提示文件格式不合法
### Requirement: Skill 数据模型
系统 SHALL 提供一个 `Skill` 数据类(dataclass 或 Pydantic model),用于表示解析后的 SKILL 实例。
#### Scenario: Skill 数据模型包含所有必要字段
- **WHEN** 创建一个 Skill 实例
- **THEN** 该实例 MUST 包含以下字段:`name`str,必填)、`description`str,必填)、`body`str,必填)、`metadata`dict,可选,默认空 dict)、`source_path`Path | None,可选)
#### Scenario: Skill 实例不可变
- **WHEN** 尝试修改已创建的 Skill 实例的 `name` 字段
- **THEN** 系统抛出异常(frozen dataclass / immutable model
### Requirement: Frontmatter 元数据提取
系统 SHALL 从 YAML frontmatter 中提取以下标准字段:`name``description``license``compatibility``metadata`(包含 `author``version``generatedBy`)。
#### Scenario: 提取完整的 metadata
- **WHEN** SKILL.md 的 frontmatter 包含 `metadata: { author: "test", version: "1.0" }`
- **THEN** 解析结果的 `metadata` 字段为 `{"author": "test", "version": "1.0"}`
#### Scenario: metadata 缺失时使用默认值
- **WHEN** SKILL.md 的 frontmatter 不包含 `metadata` 字段
- **THEN** 解析结果的 `metadata` 字段为空 dict `{}`
@@ -0,0 +1,57 @@
## ADDED Requirements
### Requirement: SKILL 目录扫描
系统 SHALL 能够扫描指定目录,自动发现并加载所有 SKILL.md 文件。
#### Scenario: 扫描包含多个 SKILL 的目录
- **WHEN** 指定一个包含 3 个子目录、每个子目录有一个 SKILL.md 文件的目录路径
- **THEN** 系统返回 3 个 Skill 实例的列表
#### Scenario: 扫描空目录
- **WHEN** 指定一个不包含任何 SKILL.md 文件的目录路径
- **THEN** 系统返回空列表,不抛出异常
#### Scenario: 扫描不存在的目录
- **WHEN** 指定一个不存在的目录路径
- **THEN** 系统抛出 `FileNotFoundError` 异常
#### Scenario: 跳过无效的 SKILL 文件
- **WHEN** 目录中包含 2 个有效 SKILL.md 和 1 个格式错误的 SKILL.md
- **THEN** 系统返回 2 个有效 Skill 实例,并通过日志记录无效文件的错误信息
### Requirement: SKILL 注册表
系统 SHALL 提供一个 SkillRegistry 类,用于管理已注册的 SKILL 实例。
#### Scenario: 注册 SKILL
- **WHEN** 调用 `registry.register(skill)` 注册一个 Skill 实例
- **THEN** 该 Skill 被存储在注册表中,可通过 `registry.get(skill.name)` 获取
#### Scenario: 注册同名 SKILL 覆盖
- **WHEN** 先后注册两个 `name` 相同但 `body` 不同的 Skill
- **THEN** 注册表中保留后注册的 Skill,前一个被覆盖
#### Scenario: 注销 SKILL
- **WHEN** 调用 `registry.unregister("skill-name")`
- **THEN** 该 SKILL 从注册表中移除,`registry.get("skill-name")` 返回 None
#### Scenario: 注销不存在的 SKILL
- **WHEN** 调用 `registry.unregister("nonexistent")`
- **THEN** 不抛出异常,静默忽略
### Requirement: 列出所有已注册 SKILL
系统 SHALL 提供查询所有已注册 SKILL 的能力。
#### Scenario: 列出所有 SKILL
- **WHEN** 注册了 3 个 SKILL 后调用 `registry.list_skills()`
- **THEN** 返回包含 3 个 Skill 实例的列表
#### Scenario: 空注册表列出 SKILL
- **WHEN** 未注册任何 SKILL 时调用 `registry.list_skills()`
- **THEN** 返回空列表
### Requirement: 从目录批量加载到注册表
系统 SHALL 提供一个便捷方法,将目录扫描和注册合并为一步操作。
#### Scenario: 从目录加载所有 SKILL
- **WHEN** 调用 `registry.load_from_directory(path)` 传入包含 3 个有效 SKILL.md 的目录
- **THEN** 注册表中包含 3 个 SKILL,可通过 `registry.list_skills()` 获取
@@ -0,0 +1,36 @@
## 1. 项目基础设施
- [x] 1.1 在 `pyproject.toml` 中添加依赖:`llama-index-core``llama-index-llms-openai``python-frontmatter`
- [x] 1.2 创建模块文件结构:`src/llama_agent_skills/skill.py``registry.py``agent.py``config.py`
- [x] 1.3 在 `config.py` 中实现配置管理:LLM API key(环境变量)、默认 SKILL 目录路径、基础 system prompt
## 2. Skill 数据模型与解析器(skill.py
- [x] 2.1 实现 `Skill` 数据类(frozen dataclass),包含字段:`name``description``body``metadata``source_path`
- [x] 2.2 实现 `SkillLoadError` 自定义异常类
- [x] 2.3 实现 `load_skill(path: Path) -> Skill` 函数:使用 `python-frontmatter` 解析 SKILL.md,校验必填字段(name、description),提取 metadata 并构造 Skill 实例
- [x] 2.4 处理解析错误场景:缺少 frontmatter、缺少必填字段、文件不存在,均抛出 `SkillLoadError`
## 3. SKILL 注册表(registry.py
- [x] 3.1 实现 `SkillRegistry` 类,包含 `register(skill)``unregister(name)``get(name)``list_skills()` 方法
- [x] 3.2 实现 `scan_directory(path: Path) -> list[Skill]` 函数:遍历目录下所有 `*/SKILL.md` 文件,调用 `load_skill` 解析,跳过无效文件并记录日志
- [x] 3.3 实现 `SkillRegistry.load_from_directory(path)` 便捷方法:调用 `scan_directory` 并批量注册
## 4. Agent 核心(agent.py
- [x] 4.1 实现 `build_system_prompt(base_prompt: str, skills: list[Skill]) -> str` 函数:按设计文档中的格式将 SKILL 内容拼接到 system prompt
- [x] 4.2 实现 `create_agent(skills, tools, llm_config) -> AgentWorkflow` 函数:使用 `AgentWorkflow.from_tools_or_functions()` 创建 Agent,传入组装后的 system prompt 和工具列表
- [x] 4.3 实现 `run_agent(agent, message) -> str` 异步函数:接受用户输入,运行 Agent 并返回响应文本
## 5. 入口整合(__init__.py
- [x] 5.1 改造 `main()` 函数:从配置读取 SKILL 目录,创建 SkillRegistry 并加载 SKILL,构建 Agent,进入交互式对话循环
- [x] 5.2 支持命令行参数或环境变量指定 SKILL 目录路径和 LLM 模型名称
## 6. 验证与测试
- [x] 6.1 使用项目中现有的 `.opencode/skills/` 目录下的 SKILL.md 文件作为测试数据,验证 skill loader 能正确解析所有 4 个 SKILL
- [x] 6.2 验证 SkillRegistry 的注册、注销、列表功能
- [x] 6.3 验证 Agent 创建后的 system prompt 包含所有已加载 SKILL 的内容
- [x] 6.4 端到端测试:加载 SKILL → 创建 Agent → 发送消息 → 获取响应
+20
View File
@@ -0,0 +1,20 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours