Update Heicode sub-mode runtime changes
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
# AI Agent 功能迁移计划
|
||||
|
||||
## 项目背景
|
||||
|
||||
将 AIExamPlatform 中的 AI agent 问答功能迁移到 AgentAPI 微服务架构中。
|
||||
|
||||
**源项目**:`/Users/mac/Projects/AIExamPlatform/AIExamPlatform/app`
|
||||
**目标项目**:`/Users/mac/Projects/AIExamPlatform/AgentAPI`
|
||||
|
||||
## 核心需求优先级
|
||||
|
||||
### P0 - 最高优先级(本计划重点)
|
||||
集成 `questionagent` 的答案增强功能:
|
||||
- 传入题目信息(题干、选项、正确答案)
|
||||
- 传入 AI 生成的答案和参考答案
|
||||
- 调用 `questionagent` 进行增强知识问答
|
||||
- 返回增强后的答案(包含教材知识点、解题策略、可视化建议等)
|
||||
|
||||
### P1 - 较低优先级(后续实现)
|
||||
- 异步题目导入功能
|
||||
- 导入过程中自动调用 AI agents 生成答案
|
||||
|
||||
---
|
||||
|
||||
## 一、迁移范围分析
|
||||
|
||||
### 1.1 核心功能模块
|
||||
|
||||
#### ✅ 已存在于 AgentAPI
|
||||
- **questionagent 子模块**:`/Users/mac/Projects/AIExamPlatform/AgentAPI/agentapi/external/questionagent`
|
||||
- `TeachingVisualAgent`:教学可视化 agent
|
||||
- `AnswerEnhancer`:答案增强器(核心功能)
|
||||
- `MinerUDocumentExplorerSkill`:教材知识点查询
|
||||
- `ProblemAnalyzer`:题目分析器
|
||||
- `SolverRegistry`:解题器注册表
|
||||
|
||||
#### 🔄 需要适配的功能
|
||||
从源项目迁移以下 agent 功能(作为参考,但核心使用 questionagent):
|
||||
- **ConversationAgent**:对话式学习(多轮对话、记忆管理)
|
||||
- **QuestionChatAgent**:题目对话(技能系统、意图识别)
|
||||
- **ExplanationAgent**:题目解析生成
|
||||
- **SimilarityAgent**:相似题目查找(基于标签的规则匹配)
|
||||
|
||||
### 1.2 依赖分析
|
||||
|
||||
#### 当前 AgentAPI 依赖
|
||||
```toml
|
||||
fastapi>=0.135.3
|
||||
sqlalchemy>=2.0.49
|
||||
pydantic>=2.12.5
|
||||
uvicorn[standard]>=0.44.0
|
||||
```
|
||||
|
||||
#### 需要新增的依赖
|
||||
```toml
|
||||
# LangChain 生态
|
||||
langchain>=0.3.25
|
||||
langchain-openai>=0.3.16
|
||||
langchain-mcp-adapters>=0.1.7
|
||||
|
||||
# OpenAI / Anthropic
|
||||
openai>=1.76.0
|
||||
anthropic>=0.94.0 # 可选,如果需要 Claude
|
||||
|
||||
# MCP 协议
|
||||
mcp>=1.18.0
|
||||
|
||||
# 其他工具
|
||||
pillow>=11.2.0 # 图像处理
|
||||
pyyaml>=6.0.2 # 配置文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、架构设计
|
||||
|
||||
### 2.1 目录结构
|
||||
|
||||
```
|
||||
AgentAPI/agentapi/
|
||||
├── external/
|
||||
│ └── questionagent/ # 已存在的 git submodule
|
||||
│ ├── src/agent/ # Agent 运行时
|
||||
│ └── src/teaching_visual_mcp/ # MCP 工具
|
||||
├── services/
|
||||
│ ├── chat_service.py # 已存在
|
||||
│ ├── agent_service.py # 新增:Agent 服务层
|
||||
│ └── answer_enhancement_service.py # 新增:答案增强服务
|
||||
├── repositories/
|
||||
│ ├── chat_repository.py # 已存在
|
||||
│ └── agent_session_repository.py # 新增:Agent 会话持久化
|
||||
├── models/
|
||||
│ ├── chat.py # 已存在
|
||||
│ ├── question.py # 已存在
|
||||
│ └── agent_session.py # 新增:Agent 会话模型
|
||||
├── http/routers/
|
||||
│ ├── chat.py # 已存在
|
||||
│ └── agents.py # 新增:Agent API 路由
|
||||
└── schemas/
|
||||
└── agent_schemas.py # 新增:Agent 请求/响应模型
|
||||
```
|
||||
|
||||
### 2.2 数据模型设计
|
||||
|
||||
#### AgentSession(新增)
|
||||
```python
|
||||
class AgentSession(Base):
|
||||
__tablename__ = "agent_sessions"
|
||||
|
||||
id: Mapped[int]
|
||||
user_id: Mapped[str]
|
||||
question_id: Mapped[int | None]
|
||||
agent_type: Mapped[str] # "answer_enhancement", "conversation", "question_chat"
|
||||
status: Mapped[str] # "active", "completed", "failed"
|
||||
metadata: Mapped[dict] # JSON 字段存储 agent 特定数据
|
||||
created_at: Mapped[datetime]
|
||||
updated_at: Mapped[datetime]
|
||||
```
|
||||
|
||||
#### AgentMessage(新增)
|
||||
```python
|
||||
class AgentMessage(Base):
|
||||
__tablename__ = "agent_messages"
|
||||
|
||||
id: Mapped[int]
|
||||
session_id: Mapped[int]
|
||||
role: Mapped[str] # "user", "assistant", "system"
|
||||
content: Mapped[str]
|
||||
metadata: Mapped[dict | None] # 存储技能使用、工具调用等信息
|
||||
created_at: Mapped[datetime]
|
||||
```
|
||||
|
||||
#### QuestionAnswer 扩展(已存在,需要利用)
|
||||
```python
|
||||
# 已有字段:
|
||||
# - answer_source: "official", "ai_generated", "ai_enhanced"
|
||||
# - content_markdown: 答案内容
|
||||
# - version_no: 版本号
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、详细实施步骤
|
||||
|
||||
### 步骤 1:环境准备与依赖安装
|
||||
|
||||
**目标**:安装必要的依赖,确保 questionagent 子模块可用
|
||||
|
||||
**操作**:
|
||||
```bash
|
||||
cd /Users/mac/Projects/AIExamPlatform/AgentAPI
|
||||
|
||||
# 添加 LangChain 和 AI 相关依赖
|
||||
uv add "langchain>=0.3.25"
|
||||
uv add "langchain-openai>=0.3.16"
|
||||
uv add "langchain-mcp-adapters>=0.1.7"
|
||||
uv add "openai>=1.76.0"
|
||||
uv add "mcp>=1.18.0"
|
||||
uv add "pillow>=11.2.0"
|
||||
uv add "pyyaml>=6.0.2"
|
||||
|
||||
# 可选:如果需要 Claude
|
||||
uv add "anthropic>=0.94.0"
|
||||
|
||||
# 同步环境
|
||||
uv sync
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ `uv.lock` 更新成功
|
||||
- ✅ 所有依赖安装无冲突
|
||||
- ✅ 可以成功 `from agent.runtime import TeachingVisualAgent`
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2:创建 Agent 服务层
|
||||
|
||||
**目标**:封装 questionagent 的答案增强功能为 AgentAPI 的服务层
|
||||
|
||||
**文件**:`agentapi/services/answer_enhancement_service.py`
|
||||
|
||||
**核心功能**:
|
||||
```python
|
||||
class AnswerEnhancementService:
|
||||
"""答案增强服务
|
||||
|
||||
封装 questionagent 的 AnswerEnhancer,提供:
|
||||
1. 题目分析
|
||||
2. 教材知识点查询
|
||||
3. 答案策略生成
|
||||
4. 可视化建议
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 初始化 questionagent 组件
|
||||
self.agent_settings = AgentSettings()
|
||||
self.mineru_skill = MinerUDocumentExplorerSkill(...)
|
||||
self.answer_enhancer = AnswerEnhancer(
|
||||
mineru_skill=self.mineru_skill,
|
||||
analyzer=ProblemAnalyzer(),
|
||||
solver_registry=build_default_solver_registry(),
|
||||
)
|
||||
|
||||
def enhance_answer(
|
||||
self,
|
||||
question_id: int,
|
||||
question_text: str,
|
||||
ai_answer: str | None,
|
||||
reference_answer: str | None,
|
||||
subject_hint: str | None = None,
|
||||
topic_hint: str | None = None,
|
||||
) -> AnswerEnhancementResult:
|
||||
"""增强答案
|
||||
|
||||
Args:
|
||||
question_id: 题目 ID
|
||||
question_text: 题目文本(题干 + 选项)
|
||||
ai_answer: AI 生成的答案
|
||||
reference_answer: 参考答案
|
||||
subject_hint: 科目提示
|
||||
topic_hint: 主题提示
|
||||
|
||||
Returns:
|
||||
增强后的答案结果
|
||||
"""
|
||||
request = AnswerEnhancementRequest(
|
||||
question=question_text,
|
||||
subject_hint=subject_hint,
|
||||
topic_hint=topic_hint,
|
||||
include_visual_plan=True,
|
||||
)
|
||||
|
||||
result = self.answer_enhancer.enhance_answer(request)
|
||||
return result
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 服务类可以成功初始化
|
||||
- ✅ `enhance_answer` 方法可以调用 questionagent
|
||||
- ✅ 返回结构化的增强结果
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3:创建数据库模型和 Repository
|
||||
|
||||
**目标**:持久化 Agent 会话和消息
|
||||
|
||||
**文件**:
|
||||
- `agentapi/models/agent_session.py`
|
||||
- `agentapi/repositories/agent_session_repository.py`
|
||||
|
||||
**核心功能**:
|
||||
```python
|
||||
# Repository
|
||||
class AgentSessionRepository:
|
||||
def create_session(
|
||||
self,
|
||||
user_id: str,
|
||||
question_id: int | None,
|
||||
agent_type: str,
|
||||
) -> AgentSession:
|
||||
"""创建 Agent 会话"""
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
session_id: int,
|
||||
role: str,
|
||||
content: str,
|
||||
metadata: dict | None = None,
|
||||
) -> AgentMessage:
|
||||
"""添加消息到会话"""
|
||||
|
||||
def get_session_history(
|
||||
self,
|
||||
session_id: int,
|
||||
) -> list[AgentMessage]:
|
||||
"""获取会话历史"""
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 数据库迁移脚本生成成功
|
||||
- ✅ 可以创建和查询 Agent 会话
|
||||
- ✅ 消息历史正确存储和检索
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4:创建 API 路由
|
||||
|
||||
**目标**:暴露答案增强功能为 RESTful API
|
||||
|
||||
**文件**:`agentapi/http/routers/agents.py`
|
||||
|
||||
**核心端点**:
|
||||
|
||||
#### 4.1 答案增强 API
|
||||
```python
|
||||
@router.post("/answer-enhancement")
|
||||
async def enhance_answer(
|
||||
request: AnswerEnhancementRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> AnswerEnhancementResponse:
|
||||
"""增强答案
|
||||
|
||||
请求示例:
|
||||
{
|
||||
"question_id": 123,
|
||||
"subject_hint": "信号与系统",
|
||||
"topic_hint": "卷积",
|
||||
"include_visual_plan": true
|
||||
}
|
||||
|
||||
响应示例:
|
||||
{
|
||||
"question_id": 123,
|
||||
"subject": "信号与系统",
|
||||
"topic": "卷积运算",
|
||||
"knowledge_points": [...],
|
||||
"key_points": ["理解卷积定义", "掌握图解法"],
|
||||
"answer_strategy": [
|
||||
{"title": "步骤1", "detail": "..."},
|
||||
{"title": "步骤2", "detail": "..."}
|
||||
],
|
||||
"answer_draft": "完整答案文本...",
|
||||
"visual_plan": {...},
|
||||
"study_advice": [...]
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
#### 4.2 Agent 会话 API(可选,用于多轮对话)
|
||||
```python
|
||||
@router.post("/sessions")
|
||||
async def create_agent_session(
|
||||
request: CreateSessionRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SessionResponse:
|
||||
"""创建 Agent 会话"""
|
||||
|
||||
@router.post("/sessions/{session_id}/messages")
|
||||
async def send_message(
|
||||
session_id: int,
|
||||
request: SendMessageRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> MessageResponse:
|
||||
"""发送消息到 Agent 会话"""
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ API 端点可以正常访问
|
||||
- ✅ 请求验证正确(Pydantic)
|
||||
- ✅ 返回结构化的增强结果
|
||||
- ✅ 错误处理完善(404, 500 等)
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5:集成到现有 Question 流程
|
||||
|
||||
**目标**:将答案增强功能集成到题目答案生成流程
|
||||
|
||||
**文件**:`agentapi/services/question_service.py`(扩展现有服务)
|
||||
|
||||
**核心功能**:
|
||||
```python
|
||||
class QuestionService:
|
||||
@staticmethod
|
||||
def generate_enhanced_answer(
|
||||
db: Session,
|
||||
question_id: int,
|
||||
user_id: str,
|
||||
) -> QuestionAnswer:
|
||||
"""为题目生成增强答案
|
||||
|
||||
流程:
|
||||
1. 查询题目信息(题干、选项、正确答案)
|
||||
2. 调用 AnswerEnhancementService
|
||||
3. 将增强结果保存为 QuestionAnswer(answer_source="ai_enhanced")
|
||||
4. 返回答案记录
|
||||
"""
|
||||
# 1. 查询题目
|
||||
question_repo = QuestionRepository(db)
|
||||
question = question_repo.get_question_with_details(question_id)
|
||||
|
||||
# 2. 构建题目文本
|
||||
question_text = _build_question_text(question)
|
||||
|
||||
# 3. 调用答案增强服务
|
||||
enhancement_service = AnswerEnhancementService()
|
||||
result = enhancement_service.enhance_answer(
|
||||
question_id=question_id,
|
||||
question_text=question_text,
|
||||
ai_answer=None, # 可选:如果已有 AI 答案
|
||||
reference_answer=_get_official_answer(question),
|
||||
subject_hint=_infer_subject(question),
|
||||
topic_hint=None,
|
||||
)
|
||||
|
||||
# 4. 保存增强答案
|
||||
answer = question_repo.create_answer(
|
||||
question_id=question_id,
|
||||
answer_source="ai_enhanced",
|
||||
content_markdown=result.answer_draft,
|
||||
metadata={
|
||||
"subject": result.subject,
|
||||
"topic": result.topic,
|
||||
"key_points": result.key_points,
|
||||
"answer_strategy": [s.model_dump() for s in result.answer_strategy],
|
||||
"visual_plan": result.visual_plan,
|
||||
"study_advice": result.study_advice,
|
||||
}
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return answer
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 可以为题目生成增强答案
|
||||
- ✅ 答案正确保存到数据库
|
||||
- ✅ metadata 字段包含完整的增强信息
|
||||
- ✅ 可以查询和展示增强答案
|
||||
|
||||
---
|
||||
|
||||
### 步骤 6:配置和环境变量
|
||||
|
||||
**目标**:配置 OpenAI API、MinerU 等外部服务
|
||||
|
||||
**文件**:`agentapi/config.py`(扩展现有配置)
|
||||
|
||||
**新增配置**:
|
||||
```python
|
||||
class Settings(BaseSettings):
|
||||
# ... 现有配置 ...
|
||||
|
||||
# OpenAI 配置
|
||||
openai_api_key: str | None = None
|
||||
openai_base_url: str | None = None
|
||||
openai_agent_model: str = "gpt-4.1-mini"
|
||||
|
||||
# Agent 配置
|
||||
agent_temperature: float = 0.0
|
||||
agent_max_iterations: int = 8
|
||||
|
||||
# MinerU 配置
|
||||
mineru_qmd_command: str = "qmd"
|
||||
mineru_default_collection: str = "textbooks"
|
||||
mineru_lookup_mode: Literal["search", "query"] = "query"
|
||||
|
||||
# 教学可视化配置
|
||||
teaching_visual_artifact_root: Path = Path(".artifacts/teaching-visuals")
|
||||
```
|
||||
|
||||
**环境变量示例**(`.env`):
|
||||
```bash
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=sk-...
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
OPENAI_AGENT_MODEL=gpt-4.1-mini
|
||||
|
||||
# MinerU(可选,如果需要教材查询)
|
||||
TVAGENT_MINERU_DEFAULT_COLLECTION=textbooks
|
||||
TVAGENT_MINERU_LOOKUP_MODE=query
|
||||
```
|
||||
|
||||
**验收标准**:
|
||||
- ✅ 配置可以从环境变量加载
|
||||
- ✅ OpenAI API 密钥正确配置
|
||||
- ✅ Agent 可以成功调用 OpenAI
|
||||
|
||||
---
|
||||
|
||||
## 四、测试计划
|
||||
|
||||
### 4.1 单元测试
|
||||
|
||||
**文件**:`tests/services/test_answer_enhancement_service.py`
|
||||
|
||||
```python
|
||||
def test_enhance_answer_basic():
|
||||
"""测试基本答案增强功能"""
|
||||
service = AnswerEnhancementService()
|
||||
result = service.enhance_answer(
|
||||
question_id=1,
|
||||
question_text="求信号 x(t) 和 h(t) 的卷积...",
|
||||
ai_answer=None,
|
||||
reference_answer="y(t) = ...",
|
||||
subject_hint="信号与系统",
|
||||
)
|
||||
|
||||
assert result.subject == "信号与系统"
|
||||
assert len(result.key_points) > 0
|
||||
assert len(result.answer_strategy) > 0
|
||||
assert result.answer_draft is not None
|
||||
```
|
||||
|
||||
### 4.2 集成测试
|
||||
|
||||
**文件**:`tests/http/test_agents_router.py`
|
||||
|
||||
```python
|
||||
def test_answer_enhancement_api(client: TestClient, db: Session):
|
||||
"""测试答案增强 API"""
|
||||
# 1. 创建测试题目
|
||||
question = create_test_question(db)
|
||||
|
||||
# 2. 调用答案增强 API
|
||||
response = client.post(
|
||||
"/api/v1/agents/answer-enhancement",
|
||||
json={
|
||||
"question_id": question.id,
|
||||
"subject_hint": "信号与系统",
|
||||
"include_visual_plan": True,
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["question_id"] == question.id
|
||||
assert "key_points" in data
|
||||
assert "answer_strategy" in data
|
||||
```
|
||||
|
||||
### 4.3 端到端测试
|
||||
|
||||
**手动测试流程**:
|
||||
1. 启动 AgentAPI 服务
|
||||
2. 使用 Postman/curl 调用答案增强 API
|
||||
3. 验证返回的增强答案质量
|
||||
4. 检查数据库中的答案记录
|
||||
|
||||
---
|
||||
|
||||
## 五、迁移优先级和时间估算
|
||||
|
||||
| 步骤 | 优先级 | 预估时间 | 依赖 |
|
||||
|------|--------|----------|------|
|
||||
| 步骤 1:依赖安装 | P0 | 0.5h | 无 |
|
||||
| 步骤 2:服务层 | P0 | 2h | 步骤 1 |
|
||||
| 步骤 3:数据模型 | P0 | 1.5h | 步骤 1 |
|
||||
| 步骤 4:API 路由 | P0 | 2h | 步骤 2, 3 |
|
||||
| 步骤 5:集成到 Question | P0 | 1.5h | 步骤 2, 3, 4 |
|
||||
| 步骤 6:配置 | P0 | 0.5h | 步骤 1 |
|
||||
| 测试 | P0 | 2h | 所有步骤 |
|
||||
|
||||
**总计**:约 10 小时(1-2 个工作日)
|
||||
|
||||
---
|
||||
|
||||
## 六、风险和注意事项
|
||||
|
||||
### 6.1 技术风险
|
||||
|
||||
1. **OpenAI API 调用失败**
|
||||
- 风险:API 密钥无效、配额不足、网络问题
|
||||
- 缓解:实现降级策略(本地 fallback)、错误重试、详细日志
|
||||
|
||||
2. **MinerU 教材查询依赖**
|
||||
- 风险:`qmd` 命令不可用、教材集合未配置
|
||||
- 缓解:使 MinerU 功能可选,提供 mock 数据用于测试
|
||||
|
||||
3. **性能问题**
|
||||
- 风险:LLM 调用耗时长(5-30秒)
|
||||
- 缓解:实现异步处理、添加超时控制、考虑缓存策略
|
||||
|
||||
### 6.2 数据一致性
|
||||
|
||||
1. **答案版本管理**
|
||||
- 问题:同一题目可能有多个 AI 生成的答案版本
|
||||
- 方案:利用 `QuestionAnswer.version_no` 和 `is_latest` 字段
|
||||
|
||||
2. **元数据存储**
|
||||
- 问题:增强结果包含复杂的嵌套结构
|
||||
- 方案:使用 JSON 字段存储 metadata,或考虑单独的表
|
||||
|
||||
### 6.3 兼容性
|
||||
|
||||
1. **questionagent 子模块更新**
|
||||
- 问题:外部子模块更新可能破坏兼容性
|
||||
- 方案:锁定子模块版本、编写适配层、充分测试
|
||||
|
||||
2. **Python 版本要求**
|
||||
- 问题:questionagent 要求 Python >=3.11,AgentAPI 要求 >=3.12
|
||||
- 方案:已兼容,无问题
|
||||
|
||||
---
|
||||
|
||||
## 七、后续扩展(P1 优先级)
|
||||
|
||||
### 7.1 异步题目导入
|
||||
|
||||
**功能**:
|
||||
- 批量导入题目时,自动调用 AI agents 生成答案
|
||||
- 使用 Celery 或 FastAPI BackgroundTasks 实现异步处理
|
||||
|
||||
**架构**:
|
||||
```python
|
||||
# 任务队列
|
||||
@celery_app.task
|
||||
def generate_answer_for_question(question_id: int):
|
||||
"""异步生成题目答案"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
QuestionService.generate_enhanced_answer(db, question_id, "system")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 导入流程
|
||||
def import_questions_batch(questions: list[dict]):
|
||||
"""批量导入题目"""
|
||||
for q_data in questions:
|
||||
# 1. 创建题目记录
|
||||
question = create_question(q_data)
|
||||
|
||||
# 2. 异步生成答案
|
||||
generate_answer_for_question.delay(question.id)
|
||||
```
|
||||
|
||||
### 7.2 其他 Agent 功能
|
||||
|
||||
- **ConversationAgent**:对话式学习(多轮对话)
|
||||
- **SimilarityAgent**:相似题目推荐
|
||||
- **QuestionChatAgent**:题目对话(技能系统)
|
||||
|
||||
---
|
||||
|
||||
## 八、成功标准
|
||||
|
||||
### 核心功能验收
|
||||
- ✅ 可以通过 API 调用答案增强功能
|
||||
- ✅ 增强答案包含教材知识点、解题策略、可视化建议
|
||||
- ✅ 答案正确保存到数据库
|
||||
- ✅ 性能可接受(单次调用 < 30秒)
|
||||
|
||||
### 代码质量
|
||||
- ✅ 代码符合 AgentAPI 架构规范(services/repositories/models/routers)
|
||||
- ✅ 类型注解完整(Python 3.12+ typing)
|
||||
- ✅ 错误处理完善
|
||||
- ✅ 日志记录清晰
|
||||
|
||||
### 文档和测试
|
||||
- ✅ API 文档完整(FastAPI 自动生成)
|
||||
- ✅ 单元测试覆盖核心逻辑
|
||||
- ✅ 集成测试验证端到端流程
|
||||
- ✅ README 包含使用说明和配置指南
|
||||
|
||||
---
|
||||
|
||||
## 九、开放问题
|
||||
|
||||
以下问题需要在实施过程中明确:
|
||||
|
||||
1. **教材集合配置**
|
||||
- 是否已有 MinerU 教材集合?
|
||||
- 教材数据存储在哪里?
|
||||
- 如何配置 `qmd` 命令?
|
||||
|
||||
2. **OpenAI API 配置**
|
||||
- 使用哪个 OpenAI 模型?(gpt-4.1-mini, gpt-4o, etc.)
|
||||
- API 密钥如何管理?(环境变量、密钥管理服务)
|
||||
- 是否需要支持其他 LLM 提供商(Claude, 本地模型)?
|
||||
|
||||
3. **答案展示**
|
||||
- 前端如何展示增强答案?
|
||||
- 是否需要支持 Markdown 渲染?
|
||||
- 可视化建议如何展示?
|
||||
|
||||
4. **性能优化**
|
||||
- 是否需要缓存增强结果?
|
||||
- 是否需要异步处理?
|
||||
- 是否需要限流?
|
||||
|
||||
5. **用户权限**
|
||||
- 哪些用户可以调用答案增强功能?
|
||||
- 是否需要计费或配额限制?
|
||||
|
||||
---
|
||||
|
||||
## 十、参考资料
|
||||
|
||||
- **questionagent README**:`/Users/mac/Projects/AIExamPlatform/AgentAPI/agentapi/external/questionagent/README.md`
|
||||
- **AgentAPI 架构**:`/Users/mac/Projects/AIExamPlatform/AgentAPI/docs/README.md`
|
||||
- **LangChain 文档**:https://python.langchain.com/
|
||||
- **MCP 协议**:https://modelcontextprotocol.io/
|
||||
@@ -0,0 +1,924 @@
|
||||
# Heicode Integration - Implementation Plan
|
||||
|
||||
**Version**: 1.0
|
||||
**Date**: 2026-05-08
|
||||
**Based on**:
|
||||
- Agent-Manager-Heicode对接需求文档(2).md v1.1
|
||||
- heicode-integration-plan.md
|
||||
- Analyst review findings
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
This plan implements the Heicode integration in 6 phases, starting with Phase 1 (Foundation & Authentication) as requested by the user. The implementation will be **fully incremental** - all new code under `/api/agnet/*` with zero changes to existing `/agents/*`, `/templates/*` endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation & Authentication (Days 1-3)
|
||||
|
||||
### 1.1 Project Structure Setup
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
api/
|
||||
├── __init__.py
|
||||
├── agnet/
|
||||
│ ├── __init__.py
|
||||
│ ├── router.py # Main FastAPI router
|
||||
│ ├── models.py # Pydantic request/response models
|
||||
│ ├── auth.py # Service token middleware
|
||||
│ ├── dependencies.py # FastAPI dependencies
|
||||
│ └── validators.py # Request validation logic
|
||||
config/
|
||||
├── __init__.py
|
||||
├── settings.py # Pydantic settings (env vars)
|
||||
└── error_codes.py # Error code enums
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
|
||||
1. **Create `config/error_codes.py`**:
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
class ErrorCode(str, Enum):
|
||||
# Authentication
|
||||
UNAUTHORIZED = "UNAUTHORIZED"
|
||||
INVALID_TOKEN = "INVALID_TOKEN"
|
||||
|
||||
# Validation
|
||||
POLICY_REJECTED = "POLICY_REJECTED"
|
||||
RESOURCE_GRANT_SECRET_REJECTED = "RESOURCE_GRANT_SECRET_REJECTED"
|
||||
MODEL_NOT_ALLOWED = "MODEL_NOT_ALLOWED"
|
||||
|
||||
# Resource limits
|
||||
BUDGET_EXCEEDED = "BUDGET_EXCEEDED"
|
||||
|
||||
# State conflicts
|
||||
DEPLOYMENT_NOT_FOUND = "DEPLOYMENT_NOT_FOUND"
|
||||
DEPLOYMENT_CONFLICT = "DEPLOYMENT_CONFLICT"
|
||||
|
||||
# Infrastructure
|
||||
INTERNAL_ERROR = "INTERNAL_ERROR"
|
||||
```
|
||||
|
||||
2. **Create `config/settings.py`**:
|
||||
```python
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Service token (Phase 1-4: pre-shared)
|
||||
HEICODE_SERVICE_TOKEN: str
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str = "sqlite:///./agent_manager.db"
|
||||
|
||||
# Redis (for idempotency)
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
IDEMPOTENCY_TTL_SECONDS: int = 86400 # 24 hours
|
||||
|
||||
# Kubernetes
|
||||
NAMESPACE_PREFIX: str = "agnet"
|
||||
|
||||
# Model gateways
|
||||
HEICODE_NEWAPI_BASE_URL: str = "https://code.xinghanlab.com"
|
||||
LITELLM_BASE_URL: str = "http://litellm-service:8000"
|
||||
|
||||
# Limits
|
||||
MAX_PAYLOAD_SIZE_MB: int = 1
|
||||
MAX_CONCURRENT_DEPLOYMENTS_PER_USER: int = 10
|
||||
MAX_CONCURRENT_DEPLOYMENTS_PER_SCOPE: int = 50
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
```
|
||||
|
||||
3. **Create `api/agnet/auth.py`** (Service token middleware):
|
||||
```python
|
||||
from fastapi import Request, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from config.settings import settings
|
||||
from config.error_codes import ErrorCode
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
security = HTTPBearer()
|
||||
|
||||
async def verify_service_token(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||||
) -> str:
|
||||
"""Verify service token from mcp-server."""
|
||||
token = credentials.credentials
|
||||
|
||||
# Phase 1-4: Simple pre-shared token validation
|
||||
if token != settings.HEICODE_SERVICE_TOKEN:
|
||||
logger.warning(f"Invalid service token attempt")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": ErrorCode.INVALID_TOKEN,
|
||||
"message": "Invalid service token",
|
||||
"request_id": None
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return token
|
||||
|
||||
def extract_headers(request: Request) -> dict:
|
||||
"""Extract required headers for correlation and audit."""
|
||||
return {
|
||||
"correlation_id": request.headers.get("X-Correlation-Id"),
|
||||
"user_id": request.headers.get("X-User-Id"),
|
||||
"binding_scope": request.headers.get("X-Binding-Scope"),
|
||||
"idempotency_key": request.headers.get("Idempotency-Key"),
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create `api/agnet/models.py`** (Pydantic models - Phase 1 subset):
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class BillingProvider(str, Enum):
|
||||
NEWAPI = "newapi"
|
||||
LITELLM = "litellm"
|
||||
|
||||
class RiskLevel(str, Enum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
success: bool = False
|
||||
error: Dict[str, Any]
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
success: bool = True
|
||||
data: Dict[str, Any]
|
||||
|
||||
# More models will be added in Phase 2
|
||||
```
|
||||
|
||||
5. **Create `api/agnet/validators.py`** (Sensitive field scanner):
|
||||
```python
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
from config.error_codes import ErrorCode
|
||||
from fastapi import HTTPException
|
||||
|
||||
SENSITIVE_KEYWORDS = [
|
||||
"password", "passwd", "pwd",
|
||||
"token", "bearer",
|
||||
"secret", "api_key", "apikey",
|
||||
"private_key", "privatekey",
|
||||
"access_key", "accesskey",
|
||||
"credential", "auth"
|
||||
]
|
||||
|
||||
def scan_for_sensitive_fields(data: Any, path: str = "") -> List[str]:
|
||||
"""Recursively scan for sensitive field names."""
|
||||
violations = []
|
||||
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
current_path = f"{path}.{key}" if path else key
|
||||
key_lower = key.lower()
|
||||
|
||||
# Check if key contains sensitive keywords
|
||||
if any(keyword in key_lower for keyword in SENSITIVE_KEYWORDS):
|
||||
violations.append(current_path)
|
||||
|
||||
# Recurse into nested structures
|
||||
violations.extend(scan_for_sensitive_fields(value, current_path))
|
||||
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data):
|
||||
violations.extend(scan_for_sensitive_fields(item, f"{path}[{i}]"))
|
||||
|
||||
return violations
|
||||
|
||||
def validate_no_sensitive_fields(payload: Dict[str, Any]) -> None:
|
||||
"""Validate that payload doesn't contain sensitive fields."""
|
||||
violations = scan_for_sensitive_fields(payload)
|
||||
|
||||
if violations:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail={
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": ErrorCode.RESOURCE_GRANT_SECRET_REJECTED,
|
||||
"message": f"Request contains sensitive fields: {', '.join(violations[:5])}",
|
||||
"details": {"violations": violations}
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
6. **Create `api/agnet/router.py`** (Main router with health check):
|
||||
```python
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from api.agnet.auth import verify_service_token, extract_headers
|
||||
from api.agnet.models import SuccessResponse
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/agnet",
|
||||
tags=["agnet"],
|
||||
dependencies=[Depends(verify_service_token)]
|
||||
)
|
||||
|
||||
@router.get("/health", response_model=SuccessResponse)
|
||||
async def health_check(request: Request):
|
||||
"""Health check endpoint for Heicode integration."""
|
||||
headers = extract_headers(request)
|
||||
logger.info(f"Health check - correlation_id={headers['correlation_id']}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"service": "agent-manager-agnet",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
7. **Update `app.py`** to include new router:
|
||||
```python
|
||||
# Add at top with other imports
|
||||
from api.agnet.router import router as agnet_router
|
||||
|
||||
# Add after existing router registrations
|
||||
app.include_router(agnet_router)
|
||||
```
|
||||
|
||||
### 1.2 Idempotency Support (Redis)
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
api/agnet/idempotency.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```python
|
||||
import redis
|
||||
import json
|
||||
from typing import Optional, Dict, Any
|
||||
from config.settings import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class IdempotencyCache:
|
||||
def __init__(self):
|
||||
self.redis_client = redis.from_url(
|
||||
settings.REDIS_URL,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
def get(self, key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cached response for idempotency key."""
|
||||
try:
|
||||
cached = self.redis_client.get(f"idempotency:{key}")
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
except Exception as e:
|
||||
logger.error(f"Redis get error: {e}")
|
||||
return None
|
||||
|
||||
def set(self, key: str, response: Dict[str, Any]) -> None:
|
||||
"""Cache response for idempotency key."""
|
||||
try:
|
||||
self.redis_client.setex(
|
||||
f"idempotency:{key}",
|
||||
settings.IDEMPOTENCY_TTL_SECONDS,
|
||||
json.dumps(response)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Redis set error: {e}")
|
||||
|
||||
idempotency_cache = IdempotencyCache()
|
||||
```
|
||||
|
||||
### 1.3 Testing Phase 1
|
||||
|
||||
**Test cases**:
|
||||
|
||||
1. **Service token validation**:
|
||||
- Valid token → 200
|
||||
- Invalid token → 401 with `INVALID_TOKEN`
|
||||
- Missing token → 401
|
||||
|
||||
2. **Health check**:
|
||||
- GET /api/agnet/health → 200 with status
|
||||
|
||||
3. **Sensitive field scanner**:
|
||||
- Payload with `password` field → 422 `RESOURCE_GRANT_SECRET_REJECTED`
|
||||
- Nested sensitive field → 422
|
||||
- Clean payload → passes
|
||||
|
||||
4. **Idempotency cache**:
|
||||
- Set and retrieve value
|
||||
- TTL expiration after 24h
|
||||
|
||||
**Acceptance criteria**:
|
||||
- [ ] Service token middleware blocks unauthorized requests
|
||||
- [ ] Headers (correlation_id, user_id, binding_scope) extracted correctly
|
||||
- [ ] Sensitive field scanner detects all keywords
|
||||
- [ ] Redis idempotency cache working
|
||||
- [ ] Health check endpoint returns 200
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Deployment Endpoints (Days 4-10)
|
||||
|
||||
### 2.1 Database Models
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
models/
|
||||
├── __init__.py
|
||||
├── deployment.py
|
||||
├── agent_instance.py
|
||||
└── base.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
|
||||
1. **Extend `database.py`** with new tables:
|
||||
```python
|
||||
# Add to existing database.py
|
||||
|
||||
class Deployment(Base):
|
||||
__tablename__ = "deployments"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
deployment_id = Column(String(100), unique=True, nullable=False, index=True)
|
||||
|
||||
# Ownership
|
||||
user_id = Column(String(100), nullable=False, index=True)
|
||||
binding_scope = Column(String(200), nullable=False, index=True)
|
||||
correlation_id = Column(String(100))
|
||||
|
||||
# Configuration
|
||||
orchestration_plan = Column(Text, nullable=False)
|
||||
risk_level = Column(String(20), nullable=False)
|
||||
approval_token = Column(Text)
|
||||
|
||||
# Budget
|
||||
budget_usd = Column(Numeric(10, 2))
|
||||
budget_consumed_usd = Column(Numeric(10, 2), default=0.00)
|
||||
|
||||
# Model gateway
|
||||
billing_provider = Column(String(50), nullable=False) # newapi | litellm
|
||||
default_model_id = Column(String(200), nullable=False)
|
||||
allowed_model_ids = Column(JSON, nullable=False)
|
||||
secret_ref = Column(String(500))
|
||||
|
||||
# Resource grants
|
||||
resource_grants = Column(JSON, default=[])
|
||||
|
||||
# Status
|
||||
status = Column(String(50), nullable=False, default="pending")
|
||||
phase = Column(String(100))
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
stopped_at = Column(DateTime)
|
||||
|
||||
# Relationships
|
||||
agent_instances = relationship("AgentInstance", back_populates="deployment", cascade="all, delete-orphan")
|
||||
|
||||
class AgentInstance(Base):
|
||||
__tablename__ = "agent_instances"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
agent_instance_id = Column(String(100), unique=True, nullable=False, index=True)
|
||||
deployment_id = Column(String(100), ForeignKey("deployments.deployment_id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# Configuration
|
||||
role = Column(String(100), nullable=False)
|
||||
phase = Column(String(100))
|
||||
|
||||
# Kubernetes
|
||||
namespace = Column(String(100), nullable=False)
|
||||
pod_name = Column(String(100), nullable=False)
|
||||
service_account = Column(String(100))
|
||||
configmap_name = Column(String(100))
|
||||
|
||||
# Status
|
||||
status = Column(String(50), nullable=False, default="pending")
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
deployment = relationship("Deployment", back_populates="agent_instances")
|
||||
```
|
||||
|
||||
### 2.2 POST /api/agnet/deployments
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
api/agnet/deployments.py
|
||||
services/deployment_orchestrator.py
|
||||
```
|
||||
|
||||
**Implementation steps**:
|
||||
|
||||
1. Define complete Pydantic models in `api/agnet/models.py`
|
||||
2. Implement validation logic (provider enum, approval check, model_id validation)
|
||||
3. Implement deployment orchestrator service
|
||||
4. Create K8s resources (namespace, ServiceAccount, ConfigMap, Deployment)
|
||||
5. Store deployment in database
|
||||
6. Return response with deployment_id
|
||||
|
||||
**Key validations**:
|
||||
- `billing_context.provider` ∈ ["newapi", "litellm"]
|
||||
- `risk_level=high` → `approval_token` required
|
||||
- `default_model_id` ∈ `allowed_model_ids`
|
||||
- Sensitive field scan
|
||||
- Idempotency check
|
||||
|
||||
### 2.3 GET /api/agnet/deployments (List)
|
||||
|
||||
**Implementation**:
|
||||
- Query deployments table with filters
|
||||
- Implement cursor-based pagination
|
||||
- Return deployment list
|
||||
|
||||
### 2.4 GET /api/agnet/deployments/{id} (Details)
|
||||
|
||||
**Implementation**:
|
||||
- Query deployment by deployment_id
|
||||
- Include agent_instances
|
||||
- Return full details
|
||||
|
||||
### 2.5 POST /api/agnet/deployments/{id}/stop
|
||||
|
||||
**Implementation**:
|
||||
- Validate deployment exists
|
||||
- Check if already stopped (idempotent)
|
||||
- Validate approval for high-risk
|
||||
- Delete K8s Deployment
|
||||
- Update status to "stopped"
|
||||
|
||||
**Acceptance criteria**:
|
||||
- [ ] POST /api/agnet/deployments creates deployment in database
|
||||
- [ ] Idempotency: same key returns same deployment_id
|
||||
- [ ] Sensitive fields rejected
|
||||
- [ ] Provider validation working
|
||||
- [ ] GET endpoints return correct data
|
||||
- [ ] Stop endpoint is idempotent
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Observability Endpoints (Days 11-15)
|
||||
|
||||
### 3.1 Event and Audit Log Models
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
models/event.py
|
||||
models/audit_log.py
|
||||
```
|
||||
|
||||
### 3.2 Log Redaction Service
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
services/log_redactor.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
|
||||
REDACTION_PATTERNS: List[Tuple[re.Pattern, str]] = [
|
||||
(re.compile(r'password["\']?\s*[:=]\s*["\']?([^"\'\s]+)', re.I), r'password=***'),
|
||||
(re.compile(r'token["\']?\s*[:=]\s*["\']?([^"\'\s]+)', re.I), r'token=***'),
|
||||
(re.compile(r'bearer\s+([A-Za-z0-9\-._~+/]+=*)', re.I), r'bearer ***'),
|
||||
(re.compile(r'api[_-]?key["\']?\s*[:=]\s*["\']?([^"\'\s]+)', re.I), r'api_key=***'),
|
||||
(re.compile(r'://([^:]+):([^@]+)@', re.I), r'://\1:***@'), # connection strings
|
||||
]
|
||||
|
||||
def redact_log_message(message: str) -> Tuple[str, bool]:
|
||||
"""Redact sensitive information from log message.
|
||||
|
||||
Returns:
|
||||
(redacted_message, was_redacted)
|
||||
"""
|
||||
redacted = message
|
||||
was_redacted = False
|
||||
|
||||
for pattern, replacement in REDACTION_PATTERNS:
|
||||
new_message = pattern.sub(replacement, redacted)
|
||||
if new_message != redacted:
|
||||
was_redacted = True
|
||||
redacted = new_message
|
||||
|
||||
return redacted, was_redacted
|
||||
```
|
||||
|
||||
### 3.3 Implement Endpoints
|
||||
|
||||
1. **GET /api/agnet/deployments/{id}/logs**
|
||||
- Fetch logs from K8s pods
|
||||
- Apply redaction
|
||||
- Return paginated logs
|
||||
|
||||
2. **GET /api/agnet/deployments/{id}/events**
|
||||
- Query events table
|
||||
- Filter by event_type, time range
|
||||
- Return paginated events
|
||||
|
||||
3. **GET /api/agnet/deployments/{id}/metrics**
|
||||
- Query K8s metrics API
|
||||
- Aggregate time-series data
|
||||
- Return metrics
|
||||
|
||||
4. **GET /api/agnet/projects/{binding_scope}/dashboard-snapshot**
|
||||
- Aggregate across all deployments in scope
|
||||
- Calculate failure rate, avg duration
|
||||
- Return snapshot
|
||||
|
||||
5. **GET /api/agnet/audit-logs**
|
||||
- Query audit_logs table
|
||||
- Filter by user_id, binding_scope, action
|
||||
- Return paginated logs
|
||||
|
||||
**Acceptance criteria**:
|
||||
- [ ] Log redaction removes all sensitive patterns
|
||||
- [ ] Logs endpoint returns paginated, redacted logs
|
||||
- [ ] Events endpoint returns structured events
|
||||
- [ ] Metrics endpoint returns time-series data
|
||||
- [ ] Dashboard snapshot aggregates correctly
|
||||
- [ ] Audit logs queryable by filters
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: K8s Integration & Pod Startup (Days 16-22)
|
||||
|
||||
### 4.1 ConfigMap Generator
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
services/configmap_generator.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
def generate_agent_md(deployment: Deployment, agent_config: dict) -> str:
|
||||
"""Generate AGENT.md natural language context."""
|
||||
return f"""# Role: {agent_config['role']}
|
||||
# Goal: {deployment.orchestration_plan}
|
||||
# Resources you can use:
|
||||
{format_resources(deployment.resource_grants)}
|
||||
# Models: {deployment.default_model_id} (allowed: {', '.join(deployment.allowed_model_ids)})
|
||||
# Forbidden:
|
||||
- Accessing resources outside granted permissions
|
||||
"""
|
||||
|
||||
def generate_resource_context(deployment: Deployment, agent_config: dict) -> dict:
|
||||
"""Generate resource_context.json (metadata, NO secrets)."""
|
||||
return {
|
||||
"agent_role": agent_config['role'],
|
||||
"deployment_id": deployment.deployment_id,
|
||||
"resources": [
|
||||
{
|
||||
"resource_id": grant['resource_id'],
|
||||
"type": grant['resource_type'],
|
||||
"secret_ref": grant['secret_ref'], # Reference only, not actual secret
|
||||
"constraints": grant.get('constraints', {})
|
||||
}
|
||||
for grant in deployment.resource_grants
|
||||
]
|
||||
}
|
||||
|
||||
def generate_permission_manifest(deployment: Deployment, agent_config: dict) -> dict:
|
||||
"""Generate permission_manifest.json (ACL for enforcement)."""
|
||||
return {
|
||||
"user_id": deployment.user_id,
|
||||
"binding_scope": deployment.binding_scope,
|
||||
"agent_role": agent_config['role'],
|
||||
"resource_grants": deployment.resource_grants
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Model Gateway Token Router
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
services/model_gateway_router.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
def get_model_gateway_env(deployment: Deployment) -> dict:
|
||||
"""Get environment variables for model gateway based on provider."""
|
||||
provider = deployment.billing_provider
|
||||
|
||||
if provider == "newapi":
|
||||
# Phase 2-4: Use fallback token (from env)
|
||||
# Phase 5: Fetch from Vault using secret_ref
|
||||
token = os.getenv("HEICODE_NEWAPI_FALLBACK_TOKEN")
|
||||
|
||||
return {
|
||||
"HEICODE_NEWAPI_BASE_URL": settings.HEICODE_NEWAPI_BASE_URL,
|
||||
"HEICODE_NEWAPI_USER_TOKEN": token
|
||||
}
|
||||
|
||||
elif provider == "litellm":
|
||||
token = os.getenv("LITELLM_FALLBACK_TOKEN")
|
||||
|
||||
return {
|
||||
"LITELLM_BASE_URL": settings.LITELLM_BASE_URL,
|
||||
"LITELLM_USER_KEY": token
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Invalid provider: {provider}")
|
||||
```
|
||||
|
||||
### 4.3 K8s Deployment Creation
|
||||
|
||||
**Update `services/deployment_orchestrator.py`**:
|
||||
|
||||
```python
|
||||
async def create_k8s_deployment(deployment: Deployment, agent_config: dict):
|
||||
"""Create K8s resources for agent deployment."""
|
||||
|
||||
# 1. Create namespace
|
||||
namespace = f"agnet-{hash_user_id(deployment.user_id)}"
|
||||
k8s_manager.create_namespace_if_not_exists(namespace)
|
||||
|
||||
# 2. Create ServiceAccount
|
||||
sa_name = f"sa-{agent_config['role']}-{hash_user_id(deployment.user_id)}"
|
||||
k8s_manager.create_service_account(namespace, sa_name)
|
||||
|
||||
# 3. Generate ConfigMap content
|
||||
agent_md = generate_agent_md(deployment, agent_config)
|
||||
resource_context = generate_resource_context(deployment, agent_config)
|
||||
permission_manifest = generate_permission_manifest(deployment, agent_config)
|
||||
|
||||
# 4. Create ConfigMap
|
||||
configmap_name = f"{deployment.deployment_id}-config"
|
||||
k8s_manager.create_configmap(
|
||||
namespace,
|
||||
configmap_name,
|
||||
{
|
||||
"AGENT.md": agent_md,
|
||||
"resource_context.json": json.dumps(resource_context),
|
||||
"permission_manifest.json": json.dumps(permission_manifest)
|
||||
}
|
||||
)
|
||||
|
||||
# 5. Get model gateway env vars
|
||||
model_gateway_env = get_model_gateway_env(deployment)
|
||||
|
||||
# 6. Create Deployment
|
||||
pod_env = {
|
||||
"VAULT_ADDR": settings.VAULT_ADDR,
|
||||
"VAULT_ROLE": sa_name,
|
||||
**model_gateway_env
|
||||
}
|
||||
|
||||
k8s_manager.create_deployment(
|
||||
namespace=namespace,
|
||||
name=f"agent-{deployment.deployment_id}",
|
||||
image=agent_config['image'],
|
||||
service_account=sa_name,
|
||||
env_vars=pod_env,
|
||||
volumes=[{
|
||||
"name": "agent-config",
|
||||
"configMap": {"name": configmap_name},
|
||||
"mountPath": "/etc/agent/"
|
||||
}],
|
||||
resources={
|
||||
"requests": {"cpu": "1000m", "memory": "2Gi"},
|
||||
"limits": {"cpu": "4000m", "memory": "8Gi"}
|
||||
}
|
||||
)
|
||||
|
||||
return namespace, sa_name, configmap_name
|
||||
```
|
||||
|
||||
**Acceptance criteria**:
|
||||
- [ ] Namespace created with correct naming
|
||||
- [ ] ServiceAccount created
|
||||
- [ ] ConfigMap contains AGENT.md, resource_context.json, permission_manifest.json
|
||||
- [ ] ConfigMap mounted to /etc/agent/ in pod
|
||||
- [ ] Model gateway env vars injected based on provider
|
||||
- [ ] NO long-term secrets in pod env
|
||||
- [ ] Pod starts successfully
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Vault Integration & SK Snapshots (Days 23-30)
|
||||
|
||||
### 5.1 Vault Client
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
services/vault_client.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
import hvac
|
||||
|
||||
class VaultClient:
|
||||
def __init__(self):
|
||||
self.client = hvac.Client(url=settings.VAULT_ADDR)
|
||||
|
||||
def get_secret(self, secret_ref: str) -> str:
|
||||
"""Fetch secret from Vault using secret_ref.
|
||||
|
||||
Args:
|
||||
secret_ref: Format "vault:secret/users/{user_id}/bindings/{scope}/..."
|
||||
"""
|
||||
# Parse secret_ref
|
||||
path = secret_ref.replace("vault:", "")
|
||||
|
||||
# Authenticate using K8s service account token
|
||||
with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as f:
|
||||
jwt = f.read()
|
||||
|
||||
self.client.auth.kubernetes.login(
|
||||
role=settings.VAULT_ROLE,
|
||||
jwt=jwt
|
||||
)
|
||||
|
||||
# Read secret
|
||||
secret = self.client.secrets.kv.v2.read_secret_version(path=path)
|
||||
return secret['data']['data']['value']
|
||||
|
||||
vault_client = VaultClient()
|
||||
```
|
||||
|
||||
### 5.2 Update Model Gateway Router
|
||||
|
||||
**Update `services/model_gateway_router.py`**:
|
||||
```python
|
||||
def get_model_gateway_env(deployment: Deployment) -> dict:
|
||||
"""Get environment variables for model gateway based on provider."""
|
||||
provider = deployment.billing_provider
|
||||
|
||||
# Phase 5: Fetch token from Vault
|
||||
token = vault_client.get_secret(deployment.secret_ref)
|
||||
|
||||
if provider == "newapi":
|
||||
return {
|
||||
"HEICODE_NEWAPI_BASE_URL": settings.HEICODE_NEWAPI_BASE_URL,
|
||||
"HEICODE_NEWAPI_USER_TOKEN": token
|
||||
}
|
||||
elif provider == "litellm":
|
||||
return {
|
||||
"LITELLM_BASE_URL": settings.LITELLM_BASE_URL,
|
||||
"LITELLM_USER_KEY": token
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 SK Snapshot Endpoints
|
||||
|
||||
**Files to create**:
|
||||
```
|
||||
api/agnet/sk_snapshots.py
|
||||
services/sk_snapshot_resolver.py
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
|
||||
1. **POST /api/agnet/sk-snapshots/resolve**
|
||||
- Parse sk_sources from deployment
|
||||
- Clone git repos (read-only)
|
||||
- Generate snapshot_id
|
||||
- Store snapshot metadata
|
||||
|
||||
2. **GET /api/agnet/deployments/{id}/sk-snapshots**
|
||||
- Query snapshot metadata
|
||||
- Return list with status
|
||||
|
||||
**Acceptance criteria**:
|
||||
- [ ] Vault client authenticates with K8s SA
|
||||
- [ ] Model gateway tokens fetched from Vault
|
||||
- [ ] SK snapshots resolved from git sources
|
||||
- [ ] Snapshot metadata stored and queryable
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Testing & Hardening (Days 31-35)
|
||||
|
||||
### 6.1 Integration Tests
|
||||
|
||||
**Test suite**:
|
||||
```
|
||||
tests/
|
||||
├── test_auth.py
|
||||
├── test_deployments.py
|
||||
├── test_observability.py
|
||||
├── test_k8s_integration.py
|
||||
├── test_vault_integration.py
|
||||
└── test_backward_compat.py
|
||||
```
|
||||
|
||||
### 6.2 Security Tests
|
||||
|
||||
1. Service token validation
|
||||
2. Sensitive field rejection
|
||||
3. Log redaction
|
||||
4. Approval validation
|
||||
5. Pod env isolation
|
||||
|
||||
### 6.3 Backward Compatibility Tests
|
||||
|
||||
1. GET /agents → 200
|
||||
2. POST /agents → creates in old namespace
|
||||
3. Old deployments unaffected
|
||||
|
||||
### 6.4 Performance Tests
|
||||
|
||||
1. Concurrent deployment creation (50 requests)
|
||||
2. Log streaming performance
|
||||
3. Metrics aggregation
|
||||
|
||||
**Acceptance criteria**:
|
||||
- [ ] All integration tests passing
|
||||
- [ ] Security tests passing
|
||||
- [ ] Backward compatibility verified
|
||||
- [ ] Performance benchmarks met
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
**Week 1 (Days 1-7)**:
|
||||
- Phase 1: Foundation & Authentication (Days 1-3)
|
||||
- Phase 2: Start Core Deployment Endpoints (Days 4-7)
|
||||
|
||||
**Week 2 (Days 8-14)**:
|
||||
- Phase 2: Complete Core Deployment Endpoints (Days 8-10)
|
||||
- Phase 3: Observability Endpoints (Days 11-14)
|
||||
|
||||
**Week 3 (Days 15-21)**:
|
||||
- Phase 3: Complete Observability (Days 15-16)
|
||||
- Phase 4: K8s Integration & Pod Startup (Days 16-21)
|
||||
|
||||
**Week 4 (Days 22-28)**:
|
||||
- Phase 4: Complete K8s Integration (Days 22-23)
|
||||
- Phase 5: Vault Integration & SK Snapshots (Days 23-28)
|
||||
|
||||
**Week 5 (Days 29-35)**:
|
||||
- Phase 5: Complete Vault Integration (Days 29-30)
|
||||
- Phase 6: Testing & Hardening (Days 31-35)
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
**External**:
|
||||
- mcp-server team: Service token format, test accounts
|
||||
- Infra team: AKS Workload Identity, Vault deployment
|
||||
- Heicode team: NewAPI endpoint, user token provisioning
|
||||
|
||||
**Internal**:
|
||||
- Redis for idempotency cache
|
||||
- PostgreSQL for new tables
|
||||
- K8s cluster access
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Backward compatibility**: All new code isolated under `/api/agnet/*`
|
||||
2. **Incremental rollout**: Phase-by-phase deployment with feature flags
|
||||
3. **Fallback tokens**: Phase 2-4 use pre-shared tokens before Vault
|
||||
4. **Testing**: Comprehensive test suite before production
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All 12 endpoints implemented
|
||||
- [ ] Service token auth working
|
||||
- [ ] Provider-based model gateway routing working
|
||||
- [ ] Log redaction working
|
||||
- [ ] Pod startup with ConfigMap working
|
||||
- [ ] Vault integration working
|
||||
- [ ] Backward compatibility maintained
|
||||
- [ ] All tests passing
|
||||
@@ -0,0 +1,623 @@
|
||||
# code_ai_agent CI/CD 工作流方案设计
|
||||
|
||||
**计划文件:** `.omc/plans/code_ai_agent_cicd.md`
|
||||
**创建日期:** 2026-03-27
|
||||
**状态:** 待用户确认
|
||||
|
||||
---
|
||||
|
||||
## 1. 方案概述
|
||||
|
||||
将 `code_ai_agent` 从单纯的代码生成服务升级为具备完整 DevOps 工作流能力的「代码员工 Agent」。新增 Git 操作、SSH 远程执行、K8s 部署触发能力,全部通过 HTTP API 暴露。
|
||||
|
||||
### 完整工作流
|
||||
|
||||
```
|
||||
外部调用方 (agent-manager / 人工)
|
||||
│
|
||||
▼
|
||||
code_ai_agent Pod
|
||||
┌──────────────────────────────────────────┐
|
||||
│ api_server.py (HTTP 路由层) │
|
||||
│ ┌──────────┬──────────┬──────────────┐ │
|
||||
│ │ /git/* │ /ssh/* │ /deploy/k8s │ │
|
||||
│ └────┬─────┴────┬─────┴──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ src/server/tools/ (工具实现层) │
|
||||
│ ┌────▼─────┐ ┌──▼──────┐ ┌──▼────────┐ │
|
||||
│ │git_tools │ │ssh_tools│ │deploy_tools│ │
|
||||
│ └────┬─────┘ └──┬──────┘ └──┬────────┘ │
|
||||
│ │ │ │ │
|
||||
│ /workspace/{task_id}/ (隔离工作空间) │
|
||||
└───┬───┴──────────┴────────────┴───────────┘
|
||||
│
|
||||
├─► Gitee (http://gitee.ath.cx:3000)
|
||||
├─► Azure VM (SSH 22)
|
||||
└─► K8s API Server
|
||||
```
|
||||
|
||||
### 典型工作流序列
|
||||
|
||||
```
|
||||
1. POST /api/v1/git/clone → 克隆仓库到 /workspace/{task_id}
|
||||
2. POST /api/v1/git/branch → 创建 feature/xxx 分支
|
||||
3. POST /api/v1/code/generate → 使用现有能力生成/修改代码
|
||||
4. POST /api/v1/git/status → 确认变更
|
||||
5. POST /api/v1/git/commit-push → 提交并推送
|
||||
6. POST /api/v1/ssh/exec → SSH 到 Azure VM 执行测试
|
||||
7. POST /api/v1/deploy/k8s → 测试通过后触发 K8s 部署
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 新增 API 端点设计(api_server.py)
|
||||
|
||||
### 2.1 Git 操作端点
|
||||
|
||||
#### `POST /api/v1/git/clone`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"repo_url": "http://gitee.ath.cx:3000/zhanggangyong/agent_management.git",
|
||||
"task_id": "task-20260327-001",
|
||||
"branch": "main",
|
||||
"depth": 1
|
||||
}
|
||||
// 响应
|
||||
{
|
||||
"success": true,
|
||||
"task_id": "task-20260327-001",
|
||||
"workspace": "/workspace/task-20260327-001",
|
||||
"branch": "main",
|
||||
"commit": "abc1234"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v1/git/branch`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"task_id": "task-20260327-001",
|
||||
"branch_name": "feature/auto-fix-bug-123",
|
||||
"from_branch": "main"
|
||||
}
|
||||
// 响应
|
||||
{ "success": true, "branch": "feature/auto-fix-bug-123", "base_commit": "abc1234" }
|
||||
```
|
||||
|
||||
#### `POST /api/v1/git/status`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{ "task_id": "task-20260327-001" }
|
||||
// 响应
|
||||
{
|
||||
"success": true,
|
||||
"branch": "feature/auto-fix-bug-123",
|
||||
"staged": ["src/main.py"],
|
||||
"unstaged": ["README.md"],
|
||||
"untracked": ["new_file.py"],
|
||||
"raw_output": "M src/main.py\n?? new_file.py"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/v1/git/commit-push`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"task_id": "task-20260327-001",
|
||||
"message": "fix: resolve null pointer in agent executor",
|
||||
"files": ["src/agent.py"],
|
||||
"push": true
|
||||
}
|
||||
// 响应
|
||||
{ "success": true, "commit": "def5678", "pushed": true, "branch": "feature/auto-fix-bug-123" }
|
||||
```
|
||||
|
||||
#### `POST /api/v1/git/diff`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{ "task_id": "task-20260327-001", "staged": false }
|
||||
// 响应
|
||||
{ "success": true, "diff": "--- a/src/main.py\n+++ b/src/main.py\n..." }
|
||||
```
|
||||
|
||||
### 2.2 SSH 操作端点
|
||||
|
||||
#### `POST /api/v1/ssh/exec`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"host": "<azure-vm-ip>",
|
||||
"user": "azureuser",
|
||||
"command": "cd /app && pytest tests/ -v --tb=short",
|
||||
"timeout": 300,
|
||||
"task_id": "task-20260327-001"
|
||||
}
|
||||
// 响应
|
||||
{
|
||||
"success": true,
|
||||
"exit_code": 0,
|
||||
"stdout": "collected 42 items ... 42 passed",
|
||||
"stderr": "",
|
||||
"duration_seconds": 45.2
|
||||
}
|
||||
```
|
||||
|
||||
**说明:** `host` 若不传,从环境变量 `SSH_TEST_HOST` 读取;`user` 从 `SSH_USER` 读取,默认 `azureuser`。
|
||||
|
||||
### 2.3 部署端点
|
||||
|
||||
#### `POST /api/v1/deploy/k8s`
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"namespace": "agent-manager",
|
||||
"deployment": "agent-manager",
|
||||
"image": "agnettaiji.azurecr.io/ai-agents/agent-manager:v1.2.3",
|
||||
"strategy": "set-image",
|
||||
"wait": true,
|
||||
"timeout": 300
|
||||
}
|
||||
// strategy: "rollout-restart" | "set-image"
|
||||
// 响应
|
||||
{ "success": true, "deployment": "agent-manager", "status": "rolled out", "duration_seconds": 62 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 新增工具函数设计(mcp_server.py + tools/ 模块)
|
||||
|
||||
### 3.1 文件结构变化
|
||||
|
||||
```
|
||||
agent_templates/agents/code_ai_agent/
|
||||
├── Dockerfile # 修改:增加 git/ssh/kubectl
|
||||
├── requirements.txt # 修改:增加 paramiko, gitpython
|
||||
├── src/server/
|
||||
│ ├── api_server.py # 修改:新增 /git /ssh /deploy 路由
|
||||
│ ├── mcp_server.py # 修改:新增工具注册
|
||||
│ ├── mcp_http_server.py # 不变
|
||||
│ └── tools/ # 新增目录
|
||||
│ ├── __init__.py
|
||||
│ ├── git_tools.py # Git 操作实现
|
||||
│ ├── ssh_tools.py # SSH 操作实现
|
||||
│ ├── deploy_tools.py # K8s 部署实现
|
||||
│ └── workspace.py # 工作空间管理
|
||||
└── k8s/ # 新增:agent 专属 K8s 配置
|
||||
├── code-ai-agent-deployment.yaml
|
||||
└── code-ai-agent-secret.yaml
|
||||
```
|
||||
|
||||
### 3.2 git_tools.py 核心接口
|
||||
|
||||
```python
|
||||
class GitTools:
|
||||
def __init__(self):
|
||||
self.workspace_root = "/workspace"
|
||||
self._gitee_user = os.getenv("GITEE_USERNAME")
|
||||
self._gitee_token = os.getenv("GITEE_TOKEN")
|
||||
|
||||
def clone(self, repo_url, task_id, branch="main", depth=1) -> dict
|
||||
def create_branch(self, task_id, branch_name, from_branch=None) -> dict
|
||||
def get_status(self, task_id) -> dict
|
||||
def stage_files(self, task_id, files=None) -> dict # None = git add -A
|
||||
def commit(self, task_id, message) -> dict
|
||||
def push(self, task_id, branch=None) -> dict
|
||||
def get_diff(self, task_id, staged=False) -> dict
|
||||
def cleanup(self, task_id) -> dict # 删除工作空间
|
||||
|
||||
def _inject_credentials(self, repo_url) -> str:
|
||||
# http://user:token@gitee.ath.cx:3000/...
|
||||
parsed = urlparse(repo_url)
|
||||
return parsed._replace(
|
||||
netloc=f"{self._gitee_user}:{self._gitee_token}@{parsed.hostname}:{parsed.port}"
|
||||
).geturl()
|
||||
|
||||
def _run(self, cmd, cwd) -> tuple[int, str, str]
|
||||
# subprocess.run,捕获 stdout/stderr,设置超时
|
||||
```
|
||||
|
||||
### 3.3 ssh_tools.py 核心接口
|
||||
|
||||
```python
|
||||
class SSHTools:
|
||||
def __init__(self):
|
||||
self._key_path = "/root/.ssh/id_rsa" # 从 Secret 挂载
|
||||
self._default_host = os.getenv("SSH_TEST_HOST")
|
||||
self._default_user = os.getenv("SSH_USER", "azureuser")
|
||||
|
||||
def exec(self, command, host=None, user=None, timeout=120, task_id=None) -> dict:
|
||||
# 使用 paramiko 连接,执行命令,返回 stdout/stderr/exit_code
|
||||
# 每次调用建立新连接,操作完毕后关闭
|
||||
|
||||
def _get_client(self, host, user) -> paramiko.SSHClient
|
||||
```
|
||||
|
||||
### 3.4 deploy_tools.py 核心接口
|
||||
|
||||
```python
|
||||
class DeployTools:
|
||||
def __init__(self):
|
||||
# 优先使用挂载的 kubeconfig,其次 in-cluster config
|
||||
self._kubeconfig = "/root/.kube/config"
|
||||
|
||||
def rollout_restart(self, namespace, deployment, wait=True, timeout=300) -> dict
|
||||
def set_image(self, namespace, deployment, image, wait=True, timeout=300) -> dict
|
||||
def get_status(self, namespace, deployment) -> dict
|
||||
def _run_kubectl(self, args) -> tuple[int, str, str]
|
||||
```
|
||||
|
||||
### 3.5 workspace.py — 工作空间管理
|
||||
|
||||
```python
|
||||
class WorkspaceManager:
|
||||
ROOT = "/workspace"
|
||||
|
||||
@staticmethod
|
||||
def get_path(task_id: str) -> str:
|
||||
# 返回 /workspace/{task_id}
|
||||
# task_id 只允许 [a-zA-Z0-9_-],防止路径注入
|
||||
|
||||
@staticmethod
|
||||
def create(task_id: str) -> str
|
||||
|
||||
@staticmethod
|
||||
def cleanup(task_id: str) -> None
|
||||
|
||||
@staticmethod
|
||||
def list_tasks() -> list[str]
|
||||
|
||||
@staticmethod
|
||||
def disk_usage() -> dict # 返回各 task_id 占用磁盘大小
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 安全设计
|
||||
|
||||
### 4.1 SSH 私钥注入
|
||||
|
||||
**方案:K8s Secret → Volume Mount(只读)**
|
||||
|
||||
```yaml
|
||||
# 新建 Secret(在 code-ai-agent 命名空间下)
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: code-ai-agent-ssh-secret
|
||||
namespace: agent-manager
|
||||
type: Opaque
|
||||
data:
|
||||
id_rsa: <base64-encoded-private-key>
|
||||
id_rsa.pub: <base64-encoded-public-key>
|
||||
known_hosts: <base64-encoded-known_hosts> # 预置 Azure VM
|
||||
|
||||
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Deployment volumeMounts
|
||||
volumeMounts:
|
||||
- name: ssh-secret
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: ssh-secret
|
||||
secret:
|
||||
secretName: code-ai-agent-ssh-secret
|
||||
defaultMode: 0400 # 私钥必须 0400,否则 SSH 拒绝
|
||||
```
|
||||
|
||||
初始化:容器 entrypoint 或 initContainer 执行 `chmod 700 /root/.ssh && chmod 600 /root/.ssh/id_rsa`。
|
||||
|
||||
### 4.2 Git 凭证安全传递
|
||||
|
||||
| 方案 | 说明 | 推荐度 |
|
||||
|------|------|--------|
|
||||
| Token 嵌入 URL | `http://user:token@host/repo` 内存拼接,不落盘 | P0 首选 |
|
||||
| git credential store | 写入 `~/.git-credentials` 文件权限 600 | 备选 |
|
||||
| SSH key for git | gitee 配置 deploy key,统一 SSH | P2 升级 |
|
||||
|
||||
实现要点:`_inject_credentials()` 在内存拼接带 token 的 URL;clone 完成后用 `git remote set-url origin <无密码URL>` 替换;日志中对 URL 做 token 脱敏。
|
||||
|
||||
### 4.3 权限隔离
|
||||
|
||||
- code_ai_agent 使用独立 ServiceAccount `code-ai-agent`
|
||||
- RBAC 只授予 `agent-manager` 命名空间下 Deployment 的 `get/patch/update`
|
||||
- SSH 连接只允许白名单 host(`SSH_ALLOWED_HOSTS` 环境变量,ssh_tools.py 校验)
|
||||
- `/workspace` 挂载独立 emptyDir,不与其他 agent 共享
|
||||
- API 通过现有 `X-API-Key` header 鉴权
|
||||
|
||||
---
|
||||
|
||||
## 5. 工作空间设计
|
||||
|
||||
### 5.1 目录结构
|
||||
|
||||
```
|
||||
/workspace/
|
||||
├── task-20260327-001/
|
||||
│ ├── agent_management/ # 克隆的仓库
|
||||
│ └── .meta.json # 任务元数据(时间、branch、状态)
|
||||
├── task-20260327-002/
|
||||
│ └── agent_management/
|
||||
└── .workspace_index.json
|
||||
```
|
||||
|
||||
### 5.2 并发隔离策略
|
||||
|
||||
- `task_id` 由调用方传入或服务端 `uuid4()` 自动生成
|
||||
- 每个 task_id 对应独立目录,无共享文件
|
||||
- 任务完成后调用清理接口或设置 TTL 自动清理
|
||||
- 磁盘告警:workspace 总占用超过 10GB 返回 503
|
||||
- `task_id` 只允许 `[a-zA-Z0-9_-]`,防止路径穿越注入
|
||||
|
||||
### 5.3 新增管理端点
|
||||
|
||||
```
|
||||
GET /api/v1/workspace/list → 列出所有 task_id 和磁盘占用
|
||||
DELETE /api/v1/workspace/{task_id} → 清理指定工作空间
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Dockerfile 修改
|
||||
|
||||
**当前状态:** 只安装 `gcc`,无 git/ssh/kubectl。
|
||||
|
||||
**修改后关键变更:**
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# 新增:git + openssh-client + curl(kubectl 安装需要)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc git openssh-client curl ca-certificates gnupg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 新增:安装 kubectl
|
||||
RUN curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
&& chmod +x kubectl && mv kubectl /usr/local/bin/
|
||||
|
||||
# 新增:paramiko(SSH)、gitpython(可选,subprocess git 为主)
|
||||
RUN pip install --no-cache-dir -r requirements.txt requests paramiko gitpython
|
||||
|
||||
# 新增:工作空间目录(PVC 挂载时会覆盖)
|
||||
RUN mkdir -p /workspace /tmp/projects
|
||||
|
||||
EXPOSE 8000 8001
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
CMD ["python", "run_api_server.py"]
|
||||
```
|
||||
|
||||
**镜像大小预估影响:** git + openssh ≈ +30MB,kubectl ≈ +50MB,paramiko ≈ +5MB。总增量约 85MB,可接受。
|
||||
|
||||
---
|
||||
|
||||
## 7. K8s 部署配置修改
|
||||
|
||||
### 7.1 新增文件:code-ai-agent-deployment.yaml
|
||||
|
||||
code_ai_agent 需要独立 Deployment(与 agent-manager 主服务分离),关键新增配置段:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
serviceAccountName: code-ai-agent
|
||||
containers:
|
||||
- name: code-ai-agent
|
||||
env:
|
||||
- name: GITEE_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: agent-manager-secret
|
||||
key: GITEE_USERNAME
|
||||
- name: GITEE_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: agent-manager-secret
|
||||
key: GITEE_TOKEN
|
||||
- name: SSH_TEST_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: code-ai-agent-ssh-secret
|
||||
key: SSH_TEST_HOST
|
||||
- name: SSH_USER
|
||||
value: "azureuser"
|
||||
volumeMounts:
|
||||
- name: ssh-secret
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
- name: kubeconfig
|
||||
mountPath: /root/.kube
|
||||
readOnly: true
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "300m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
volumes:
|
||||
- name: ssh-secret
|
||||
secret:
|
||||
secretName: code-ai-agent-ssh-secret
|
||||
defaultMode: 0400
|
||||
- name: kubeconfig
|
||||
secret:
|
||||
secretName: kubeconfig-secret
|
||||
optional: true
|
||||
- name: workspace
|
||||
emptyDir:
|
||||
sizeLimit: 20Gi
|
||||
```
|
||||
|
||||
### 7.2 agent-manager-secret 新增 key
|
||||
|
||||
在现有 `k8s/agent-manager-secret.yaml` 补充:
|
||||
```yaml
|
||||
GITEE_USERNAME: "zhanggangyong"
|
||||
```
|
||||
|
||||
### 7.3 新建 code-ai-agent-ssh-secret.yaml
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: code-ai-agent-ssh-secret
|
||||
namespace: agent-manager
|
||||
type: Opaque
|
||||
data:
|
||||
id_rsa: <base64-encoded-private-key>
|
||||
known_hosts: <base64-encoded-known_hosts>
|
||||
SSH_TEST_HOST: <base64-encoded-azure-vm-ip>
|
||||
```
|
||||
|
||||
### 7.4 RBAC 新增 Role + RoleBinding
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: code-ai-agent-role
|
||||
namespace: agent-manager
|
||||
rules:
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 实现优先级
|
||||
|
||||
### P0 — 核心能力(第一阶段,必须先完成)
|
||||
|
||||
| 编号 | 内容 | 验收标准 |
|
||||
|------|------|----------|
|
||||
| P0-1 | Dockerfile 安装 git + openssh-client + kubectl | `docker run ... git --version` 输出正常 |
|
||||
| P0-2 | workspace.py 工作空间管理 | 单元测试覆盖路径注入防护(task_id 含 `../` 时拒绝)|
|
||||
| P0-3 | git_tools.py:clone + branch + status + commit + push | 成功 clone gitee 仓库,创建分支并推送 |
|
||||
| P0-4 | ssh_tools.py:exec | SSH 到 Azure VM 执行 `echo ok`,返回 exit_code=0 |
|
||||
| P0-5 | api_server.py 新增 /git/* 和 /ssh/exec 路由 | HTTP 调用返回正确 JSON,异常时返回 4xx/5xx |
|
||||
| P0-6 | SSH Secret + Volume Mount K8s 配置 | Pod 启动后 `/root/.ssh/id_rsa` 权限为 0400 |
|
||||
|
||||
### P1 — 完整工作流(第二阶段)
|
||||
|
||||
| 编号 | 内容 | 验收标准 |
|
||||
|------|------|----------|
|
||||
| P1-1 | deploy_tools.py:rollout-restart + set-image | 成功触发 K8s 滚动更新,等待就绪返回 |
|
||||
| P1-2 | api_server.py 新增 /deploy/k8s 路由 | 调用后 deployment 完成更新,status 字段正确 |
|
||||
| P1-3 | RBAC:code-ai-agent ServiceAccount + Role | `kubectl auth can-i patch deployment` 返回 yes |
|
||||
| P1-4 | git diff 接口 | 返回正确 unified diff 格式 |
|
||||
| P1-5 | workspace list/cleanup 管理端点 | GET /workspace/list 返回含磁盘占用的列表 |
|
||||
| P1-6 | mcp_server.py 注册新工具 | MCP 工具列表中出现 git_clone、ssh_exec、k8s_deploy |
|
||||
|
||||
### P2 — 增强与优化(第三阶段)
|
||||
|
||||
| 编号 | 内容 | 说明 |
|
||||
|------|------|------|
|
||||
| P2-1 | 替换 HTTP token 为 SSH key 方式访问 git | 更安全,需 gitee 配置 deploy key |
|
||||
| P2-2 | workspace 磁盘告警 + TTL 自动清理 | 防止 emptyDir 耗尽,定时任务每小时扫描 |
|
||||
| P2-3 | SSH 连接池(paramiko Transport 复用) | 减少高频调用连接建立开销 |
|
||||
| P2-4 | /api/v1/pipeline/run 编排端点 | 单次调用完成 clone→修改→测试→部署全流程 |
|
||||
| P2-5 | 操作审计日志(structured log) | 所有 git/ssh/deploy 操作可追溯,含 task_id |
|
||||
|
||||
---
|
||||
|
||||
## 9. 潜在风险与注意事项
|
||||
|
||||
### 风险 1:Git Token 泄露
|
||||
- **场景:** token 嵌入 URL 后被 `git remote -v`、进程环境变量或日志打印
|
||||
- **缓解:** clone 后立即 `git remote set-url origin <无密码URL>`;日志中 URL 做正则脱敏;不将 token 写入任何文件
|
||||
|
||||
### 风险 2:workspace 磁盘耗尽
|
||||
- **场景:** 大量任务未清理,emptyDir 超限导致 Pod 被驱逐
|
||||
- **缓解:** emptyDir 设 `sizeLimit: 20Gi`;API 层磁盘检查(超 10GB 返回 503);P2 阶段加 TTL 自动清理
|
||||
|
||||
### 风险 3:SSH 私钥被容器内进程读取
|
||||
- **场景:** 容器内其他进程或代码执行漏洞读取 `/root/.ssh/id_rsa`
|
||||
- **缓解:** Volume `defaultMode: 0400`;容器以非 root 用户运行(P2 阶段);考虑使用 Vault Agent Injector 替代 Secret Volume
|
||||
|
||||
### 风险 4:K8s 部署权限过宽
|
||||
- **场景:** code_ai_agent 被攻击后可滥用 kubectl 权限影响其他服务
|
||||
- **缓解:** RBAC 严格限制到 `agent-manager` 命名空间,只允许 get/patch/update Deployment;禁止 delete、exec、secret 等危险操作
|
||||
|
||||
### 风险 5:并发 git 操作冲突
|
||||
- **场景:** 两个任务使用相同 task_id 或同一仓库并发操作
|
||||
- **缓解:** task_id 全局唯一(UUID);每个 task_id 独立目录;api_server.py 对同一 task_id 的写操作加文件锁
|
||||
|
||||
### 风险 6:Azure VM SSH 连接超时或不可达
|
||||
- **场景:** 网络抖动或 VM 重启导致 SSH 命令挂起
|
||||
- **缓解:** paramiko 设置 `banner_timeout`、`auth_timeout`、`timeout`;所有 ssh.exec 调用强制设置 `timeout` 参数(默认 120s);超时后返回明确错误而非挂起
|
||||
|
||||
### 风险 7:CI/CD 循环触发
|
||||
- **场景:** code_ai_agent 推送代码触发 CI,CI 再触发 code_ai_agent,形成死循环
|
||||
- **缓解:** commit message 加 `[skip-ci]` 标记;部署端点需要明确的 image tag 参数,不自动推断
|
||||
|
||||
---
|
||||
|
||||
## 10. 工作计划(Task Flow)
|
||||
|
||||
### Step 1:基础设施准备(P0-1, P0-6)
|
||||
- 修改 `Dockerfile`,安装 git/openssh/kubectl
|
||||
- 创建 `code-ai-agent-ssh-secret.yaml`
|
||||
- 更新 `agent-manager-secret.yaml` 补充 `GITEE_USERNAME`
|
||||
- **验收:** Pod 启动正常,`/root/.ssh/id_rsa` 权限 0400
|
||||
|
||||
### Step 2:工作空间与 Git 工具(P0-2, P0-3)
|
||||
- 实现 `src/server/tools/workspace.py`
|
||||
- 实现 `src/server/tools/git_tools.py`
|
||||
- 编写单元测试
|
||||
- **验收:** 能 clone gitee 仓库,创建分支,commit+push
|
||||
|
||||
### Step 3:SSH 工具与 API 路由(P0-4, P0-5)
|
||||
- 实现 `src/server/tools/ssh_tools.py`
|
||||
- 在 `api_server.py` 注册 `/git/*` 和 `/ssh/exec` 路由
|
||||
- **验收:** HTTP 调用 clone + ssh exec 全流程通
|
||||
|
||||
### Step 4:部署工具与完整流程(P1-1 ~ P1-3)
|
||||
- 实现 `src/server/tools/deploy_tools.py`
|
||||
- 注册 `/deploy/k8s` 路由
|
||||
- 配置 RBAC
|
||||
- **验收:** 调用 `/deploy/k8s` 触发滚动更新成功
|
||||
|
||||
### Step 5:MCP 工具注册与增强(P1-4 ~ P1-6, P2)
|
||||
- 在 `mcp_server.py` 注册新工具
|
||||
- workspace 管理端点
|
||||
- 按需推进 P2 优化项
|
||||
|
||||
---
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. 完整工作流(clone → branch → 代码修改 → commit/push → SSH 测试 → K8s 部署)可通过 HTTP API 驱动,无人工干预
|
||||
2. 所有凭证(git token、SSH 私钥)通过 K8s Secret 注入,不硬编码
|
||||
3. 并发多任务互不干扰(task_id 隔离)
|
||||
4. 单个操作失败有明确错误信息,不影响其他任务
|
||||
5. Pod 重启后工作空间可按需重建(无状态设计)
|
||||
|
||||
---
|
||||
|
||||
**Does this plan capture your intent?**
|
||||
- `proceed` — 开始实现,移交 executor
|
||||
- `adjust [X]` — 返回调整某个模块设计
|
||||
- `restart` — 废弃重新开始
|
||||
@@ -0,0 +1,310 @@
|
||||
# Heicode Integration Development Plan
|
||||
|
||||
**Based on**: Agent-Manager-Heicode对接需求文档(2).md v1.1
|
||||
**Target**: Implement 12 new `/api/agnet/*` endpoints + Pod startup changes
|
||||
**Timeline**: 3-4 weeks (5 phases)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation & Authentication (2-3 days)
|
||||
|
||||
### 1.1 Service Token Authentication
|
||||
- [ ] Add service token validation middleware
|
||||
- [ ] Support `Authorization: Bearer <token>` header validation
|
||||
- [ ] Implement token verification (start with pre-shared token, option A)
|
||||
- [ ] Add correlation/request ID tracking (`X-Correlation-Id`, `X-User-Id`, `X-Binding-Scope`)
|
||||
- [ ] Add `Idempotency-Key` support with caching mechanism
|
||||
|
||||
### 1.2 Error Response Structure
|
||||
- [ ] Implement standardized error response format:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "POLICY_REJECTED",
|
||||
"message": "human readable",
|
||||
"request_id": "req_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Define error code constants (POLICY_REJECTED, BUDGET_EXCEEDED, MODEL_NOT_ALLOWED, etc.)
|
||||
- [ ] Add error code mapping and response helpers
|
||||
|
||||
### 1.3 Project Structure
|
||||
- [ ] Create `/api/agnet` router module
|
||||
- [ ] Set up request/response models (Pydantic schemas)
|
||||
- [ ] Add logging infrastructure with correlation ID support
|
||||
- [ ] Set up configuration for new endpoints (separate from existing `/agents/*`)
|
||||
|
||||
**Deliverable**: Service token auth working, error responses standardized
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Deployment Endpoints (5-7 days)
|
||||
|
||||
### 2.1 POST /api/agnet/deployments (Create)
|
||||
- [ ] Implement request payload validation:
|
||||
- Required fields: `orchestration_plan`, `agents[]`, `risk_level`, `budget`, `metadata.correlation_id`
|
||||
- Validate `billing_context.provider` enum (`newapi` | `litellm`)
|
||||
- Validate `resource_grants[]` structure
|
||||
- Validate `default_model_id` ∈ `allowed_model_ids`
|
||||
- [ ] Implement sensitive field rejection (recursive scan for password/token/secret/private_key/access_key)
|
||||
- [ ] Implement approval validation for `risk_level=high`
|
||||
- [ ] Add idempotency check (return existing result if same key)
|
||||
- [ ] Return deployment response with `deployment_id`, `status`, `agent_instances[]`
|
||||
|
||||
### 2.2 GET /api/agnet/deployments (List)
|
||||
- [ ] Implement pagination with cursor support
|
||||
- [ ] Filter by `user_id`, `binding_scope`, `status`
|
||||
- [ ] Return deployment list with basic info
|
||||
|
||||
### 2.3 GET /api/agnet/deployments/{id} (Details)
|
||||
- [ ] Return full deployment details
|
||||
- [ ] Include agent instances with current phase
|
||||
- [ ] Include resource grants summary
|
||||
|
||||
### 2.4 POST /api/agnet/deployments/{id}/stop (Stop)
|
||||
- [ ] Implement idempotent stop logic
|
||||
- [ ] Handle already-stopped deployments (200 + status=stopped)
|
||||
- [ ] Handle terminal state conflicts (409 DEPLOYMENT_CONFLICT)
|
||||
- [ ] Validate approval for high-risk stops
|
||||
|
||||
**Deliverable**: Core CRUD endpoints working with mock K8s backend
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Observability Endpoints (3-5 days)
|
||||
|
||||
### 3.1 GET /api/agnet/deployments/{id}/logs
|
||||
- [ ] Implement log retrieval from K8s pods
|
||||
- [ ] **Mandatory log redaction**: scan and mask passwords/tokens/keys/connection strings
|
||||
- [ ] Support query params: `agent_instance_id`, `stream`, `since`, `limit`, `cursor`
|
||||
- [ ] Return structured log entries with `log_id`, `stream`, `level`, `message`, `redacted`, `occurred_at`
|
||||
|
||||
### 3.2 GET /api/agnet/deployments/{id}/logs/stream (Optional SSE)
|
||||
- [ ] Implement SSE streaming for real-time logs
|
||||
- [ ] Apply same redaction rules as batch logs
|
||||
- [ ] Handle client disconnection gracefully
|
||||
|
||||
### 3.3 GET /api/agnet/deployments/{id}/events
|
||||
- [ ] Implement event storage/retrieval
|
||||
- [ ] Support event types: `deployment.accepted`, `instance.phase_changed`, `sk_snapshot_refreshed`, `resource_grant.attached/revoked`, `budget.threshold_reached`, `deployment.failed`
|
||||
- [ ] Support filtering by event type, time range
|
||||
- [ ] Return structured events with `event_id`, `event`, `correlation_id`, `occurred_at`
|
||||
|
||||
### 3.4 GET /api/agnet/deployments/{id}/metrics
|
||||
- [ ] Implement time-series metrics retrieval
|
||||
- [ ] Support metrics: `tokens_used`, `cost_usd`, `duration_sec`, `cpu_millicores`, `memory_mb`, `restart_count`, `tool_call_count`, `error_count`, `queue_latency_ms`
|
||||
- [ ] Support `window` and `step` parameters
|
||||
|
||||
### 3.5 GET /api/agnet/projects/{binding_scope}/dashboard-snapshot
|
||||
- [ ] Aggregate metrics across deployments in binding_scope
|
||||
- [ ] Return: `active_instances`, `phase_distribution`, `failure_rate_1h`, `avg_task_duration`, `budget`, `resource_usage`, `updated_at`
|
||||
|
||||
### 3.6 GET /api/agnet/audit-logs
|
||||
- [ ] Implement audit log storage/retrieval
|
||||
- [ ] Support filtering by `user_id`, `binding_scope`, `actor`, `action`, `since`
|
||||
- [ ] Return structured audit entries with `audit_id`, `actor`, `action`, `resource`, `result`, `occurred_at`
|
||||
|
||||
**Deliverable**: All observability endpoints working with real K8s data
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: K8s Integration & Pod Startup (5-7 days)
|
||||
|
||||
### 4.1 K8s Deployment Creation
|
||||
- [ ] Implement K8s client integration
|
||||
- [ ] Create namespace strategy: `agnet-{user_id_hash}` (separate from old namespaces)
|
||||
- [ ] Create ServiceAccount per deployment: `sa-{role}-{user_id_hash}`
|
||||
- [ ] Bind SA to Vault Kubernetes Auth role
|
||||
|
||||
### 4.2 ConfigMap Generation
|
||||
- [ ] Generate `AGENT.md` from deployment payload (natural language context)
|
||||
- [ ] Generate `resource_context.json` (structured metadata, NO secrets)
|
||||
- [ ] Generate `permission_manifest.json` (structured permissions for enforcement)
|
||||
- [ ] Create ConfigMap and mount to Pod at `/etc/agent/`
|
||||
|
||||
### 4.3 Model Gateway Token Routing (v1.1 Critical)
|
||||
- [ ] Implement provider-based token routing:
|
||||
- `provider=newapi`:
|
||||
- Fetch token from `secret_ref` (Vault or fallback)
|
||||
- Inject env: `HEICODE_NEWAPI_BASE_URL=https://code.xinghanlab.com`
|
||||
- Inject env: `HEICODE_NEWAPI_USER_TOKEN=<token>`
|
||||
- `provider=litellm`:
|
||||
- Fetch token from `secret_ref` (Vault or fallback)
|
||||
- Inject env: `LITELLM_BASE_URL=<internal_litellm_url>`
|
||||
- Inject env: `LITELLM_USER_KEY=<token>`
|
||||
- [ ] Add fallback for Phase 2-3 testing (pre-shared token with annotation)
|
||||
- [ ] Annotate deployment with `heicode.io/token-source` and `secret_ref` for audit
|
||||
|
||||
### 4.4 Pod Environment Setup
|
||||
- [ ] Inject Vault env vars: `VAULT_ADDR`, `VAULT_AUTH_PATH`, `VAULT_ROLE`
|
||||
- [ ] Inject model gateway env vars (based on provider)
|
||||
- [ ] **NO long-term secrets in env** (enforce in code review)
|
||||
- [ ] Mount ConfigMap volumes
|
||||
|
||||
### 4.5 Deployment Spec
|
||||
- [ ] Create Deployment with:
|
||||
- `serviceAccountName`: SA created in 4.1
|
||||
- `volumeMounts`: ConfigMap from 4.2
|
||||
- `env`: Vault + model gateway vars from 4.3-4.4
|
||||
- Container image, resource limits, health checks
|
||||
- [ ] Track deployment status and update internal state
|
||||
|
||||
**Deliverable**: Real K8s pods launching with correct configuration
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Vault Integration & SK Snapshots (1-2 weeks)
|
||||
|
||||
### 5.1 AKS Workload Identity Setup (with infra team)
|
||||
- [ ] Enable OIDC issuer + Workload Identity addon on AKS
|
||||
- [ ] Configure ServiceAccount annotations: `azure.workload.identity/client-id`
|
||||
- [ ] Set up Federated Identity Credential in Azure AD
|
||||
|
||||
### 5.2 Vault Kubernetes Auth
|
||||
- [ ] Configure Vault policies per `(user_id, binding_scope)`:
|
||||
```hcl
|
||||
path "secret/users/${user_id}/bindings/${binding_scope}/resources/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
```
|
||||
- [ ] Configure Vault Kubernetes Auth roles binding SA → policy
|
||||
- [ ] Test Pod → Vault authentication flow
|
||||
|
||||
### 5.3 Secret Retrieval
|
||||
- [ ] Implement Vault client in agent-manager
|
||||
- [ ] Fetch model gateway tokens from Vault using `secret_ref`
|
||||
- [ ] Remove fallback pre-shared token path (Phase 2-3 temporary)
|
||||
- [ ] Add token TTL tracking and refresh logic
|
||||
|
||||
### 5.4 SK Snapshot Endpoints
|
||||
- [ ] POST /api/agnet/sk-snapshots/resolve:
|
||||
- Parse `agents[].sk_sources[]` (git/upload resources)
|
||||
- Fetch resources and generate read-only snapshot
|
||||
- Generate `snapshot_id`, `artifact_ref`, `checksum`
|
||||
- Store snapshot metadata
|
||||
- [ ] GET /api/agnet/deployments/{id}/sk-snapshots:
|
||||
- Return snapshots list with `source_ref`, `resolved_at`, `status`
|
||||
|
||||
**Deliverable**: Full Vault integration, SK snapshots working
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Testing & Hardening (1 week)
|
||||
|
||||
### 6.1 Security Testing
|
||||
- [ ] Test service token validation (401 on invalid token)
|
||||
- [ ] Test sensitive field rejection (422 on plaintext secrets)
|
||||
- [ ] Test log redaction (no secrets in log output)
|
||||
- [ ] Test approval validation for high-risk operations
|
||||
- [ ] Test Pod env isolation (no long-term secrets)
|
||||
|
||||
### 6.2 Integration Testing
|
||||
- [ ] Test full deployment flow: create → running → logs → metrics → stop
|
||||
- [ ] Test both `provider=newapi` and `provider=litellm` paths
|
||||
- [ ] Test idempotency (same Idempotency-Key returns same result)
|
||||
- [ ] Test error handling (all error codes)
|
||||
- [ ] Test pagination and filtering
|
||||
|
||||
### 6.3 Backward Compatibility Testing
|
||||
- [ ] Verify existing `/agents/*` endpoints still work
|
||||
- [ ] Verify old taiji deployments unaffected
|
||||
- [ ] Verify namespace isolation (old vs new)
|
||||
|
||||
### 6.4 Performance Testing
|
||||
- [ ] Test concurrent deployment creation
|
||||
- [ ] Test log streaming performance
|
||||
- [ ] Test metrics aggregation performance
|
||||
|
||||
**Deliverable**: Production-ready implementation
|
||||
|
||||
---
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
### Documentation
|
||||
- [ ] API documentation (OpenAPI/Swagger)
|
||||
- [ ] Deployment guide for ops team
|
||||
- [ ] Security review checklist
|
||||
- [ ] Runbook for common issues
|
||||
|
||||
### Monitoring
|
||||
- [ ] Add metrics for new endpoints (latency, error rate)
|
||||
- [ ] Add alerts for deployment failures
|
||||
- [ ] Add audit logging for all operations
|
||||
|
||||
### Configuration
|
||||
- [ ] Environment variables for Vault, K8s, model gateways
|
||||
- [ ] Feature flags for gradual rollout
|
||||
- [ ] Configuration validation on startup
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Blockers
|
||||
|
||||
### External Dependencies
|
||||
- **mcp-server team**: Service token format, test accounts, APIM routing
|
||||
- **Infra team**: AKS Workload Identity setup, Vault deployment, network policies
|
||||
- **Heicode team**: NewAPI endpoint, user token provisioning
|
||||
|
||||
### Decision Points
|
||||
- [ ] Service token scheme: A (pre-shared) vs B (JWT) vs C (Workload Identity)
|
||||
- **Recommendation**: Start with A, migrate to C in Phase 5
|
||||
- [ ] Staging environment base URL for mcp-server
|
||||
- [ ] Model gateway fallback token limits ($1/day for testing)
|
||||
|
||||
---
|
||||
|
||||
## Rollout Strategy
|
||||
|
||||
### Phase 2-3: Mock Backend
|
||||
- New endpoints return mock data
|
||||
- No real K8s operations
|
||||
- Focus on contract validation
|
||||
|
||||
### Phase 4: Staging K8s
|
||||
- Real K8s deployments in staging cluster
|
||||
- Pre-shared tokens for model gateways
|
||||
- Limited user testing
|
||||
|
||||
### Phase 5: Production
|
||||
- Vault integration complete
|
||||
- Full security hardening
|
||||
- Gradual rollout with feature flags
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All 12 endpoints implemented and tested
|
||||
- [ ] Pod startup follows security requirements (no long-term secrets)
|
||||
- [ ] Both `provider=newapi` and `provider=litellm` paths working
|
||||
- [ ] Log redaction working (no secrets leaked)
|
||||
- [ ] Backward compatibility maintained (old endpoints unchanged)
|
||||
- [ ] Integration tests passing with mcp-server
|
||||
- [ ] Security review approved
|
||||
- [ ] Production deployment successful
|
||||
|
||||
---
|
||||
|
||||
## Timeline Summary
|
||||
|
||||
| Phase | Duration | Key Deliverable |
|
||||
|-------|----------|-----------------|
|
||||
| Phase 1 | 2-3 days | Auth & error handling |
|
||||
| Phase 2 | 5-7 days | Core CRUD endpoints |
|
||||
| Phase 3 | 3-5 days | Observability endpoints |
|
||||
| Phase 4 | 5-7 days | K8s integration |
|
||||
| Phase 5 | 1-2 weeks | Vault + SK snapshots |
|
||||
| Phase 6 | 1 week | Testing & hardening |
|
||||
| **Total** | **3-4 weeks** | Production-ready |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review plan with team
|
||||
2. Confirm service token scheme with mcp-server team
|
||||
3. Set up staging environment
|
||||
4. Start Phase 1 implementation
|
||||
@@ -0,0 +1,7 @@
|
||||
## code_ai_agent_cicd - 2026-03-27
|
||||
- [ ] Azure VM 的 IP 地址和 SSH 用户名是什么? — 需要填入 code-ai-agent-ssh-secret 的 SSH_TEST_HOST 字段
|
||||
- [ ] SSH 私钥是否已存在?还是需要新生成并将公钥部署到 Azure VM? — 影响 Secret 创建流程
|
||||
- [ ] code_ai_agent 是否有专属 Deployment?还是目前通过 agent-manager 动态启动? — 决定是新建 Deployment 还是修改现有配置
|
||||
- [ ] 测试命令是什么(Azure VM 上执行)?例如 `pytest tests/` 还是其他脚本? — 影响 SSH exec 的默认命令设计
|
||||
- [ ] K8s 部署触发后,image tag 如何确定?是调用方传入还是从 CI 环境变量读取? — 影响 /deploy/k8s 接口设计
|
||||
- [ ] GITEE_USERNAME 是否已在 agent-manager-secret 中?当前 secret.yaml 中未见此 key — 需确认后补充
|
||||
Reference in New Issue
Block a user