Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions .github/workflows/docker-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,39 @@ name: Docker Build Validation
on:
push:
paths:
- 'agent-sdk-server/Dockerfile'
- 'agent-sdk-server/**'
pull_request:
paths:
- 'agent-sdk-server/Dockerfile'
- 'agent-sdk-server/**'

jobs:
validate-docker:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build and validate file permissions
run: |
cd agent-sdk-server
docker build -t agent-sdk-server:test .
- name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: agent-sdk-server/Dockerfile

# Simulate Lambda init - copy from /opt to /tmp
docker run --rm agent-sdk-server:test python -c "
import shutil
from pathlib import Path
build:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4

src = Path('/opt/claude-config')
dst = Path('/tmp/.claude-code')
dst.mkdir(exist_ok=True)
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

for item in src.iterdir():
target = dst / item.name
if item.is_dir():
shutil.copytree(item, target, dirs_exist_ok=True)
else:
shutil.copy2(item, target)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

print('Lambda init simulation: OK')
"
- name: Build ARM64 image
uses: docker/build-push-action@v6
with:
context: ./agent-sdk-server
platforms: linux/arm64
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
- Subagent的"tools"字段定义不支持通配符,而是需要具体的Tools名称才可以.
- template.yaml中引用Parameter时必须使用`!Ref`而非字面字符串`'${ParamName}'`.
- agents/*.md文件必须包含YAML frontmatter并定义`name`字段,否则SDK会跳过加载.
- Lambda容器中uvx不可用,需要在Dockerfile中创建符号链接或使用uv安装脚本.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Telegram User → Bot API → API Gateway → sdk-client Lambda
- **Session 持久化**:DynamoDB 存储映射,S3 存储对话历史,支持跨请求恢复
- **多租户隔离**:基于 Telegram chat_id + thread_id 实现客户端隔离
- **SubAgent 支持**:可配置多个专业 Agent(如 AWS 支持)
- **Skills 支持**:可复用的技能模块(计划中)
- **Skills 支持**:可复用的技能模块
- **MCP 集成**:支持 HTTP 和本地命令类型的 MCP 服务器
- **自动清理**:25天 TTL + S3 生命周期管理

Expand All @@ -33,7 +33,8 @@ Telegram User → Bot API → API Gateway → sdk-client Lambda
│ └── claude-config/ # 配置文件
│ ├── agents.json # SubAgent定义
│ ├── mcp.json # MCP服务器配置
│ ├── skills/ # Skills定义(计划中)
│ ├── skills/ # Skills定义
│ │ └── hello-world/ # 示例 Skill
│ └── system_prompt.md # 系统提示
├── agent-sdk-client/ # Telegram客户端 (ZIP部署)
Expand Down Expand Up @@ -121,7 +122,6 @@ sam deploy --guided

## TODO

- [ ] 实现 Skills 支持(参考 `docs/anthropic-agent-sdk-official/skills-in-sdk.md`)
- [ ] 多租户 TenantID 隔离

## License
Expand Down
31 changes: 24 additions & 7 deletions agent-sdk-client/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import httpx
from telegram import Bot, Update
from telegram.constants import ParseMode, ChatAction
from telegram.helpers import escape_markdown
from telegram.error import BadRequest

from config import Config

Expand Down Expand Up @@ -84,10 +86,25 @@ async def process_webhook(body: dict) -> None:
if len(text) > 4000:
text = text[:4000] + "\n\n... (truncated)"

await bot.send_message(
chat_id=message.chat_id,
text=text,
parse_mode=ParseMode.MARKDOWN,
message_thread_id=message.message_thread_id,
reply_to_message_id=message.message_id,
)
# Try MARKDOWN_V2 first, fallback with escape on parse errors
try:
await bot.send_message(
chat_id=message.chat_id,
text=text,
parse_mode=ParseMode.MARKDOWN_V2,
message_thread_id=message.message_thread_id,
reply_to_message_id=message.message_id,
)
except BadRequest as e:
if "parse entities" in str(e).lower():
print(f"[MARKDOWN_V2] Parse error: {e}, retrying with escaped text")
safe_text = escape_markdown(text, version=2)
await bot.send_message(
chat_id=message.chat_id,
text=safe_text,
parse_mode=ParseMode.MARKDOWN_V2,
message_thread_id=message.message_thread_id,
reply_to_message_id=message.message_id,
)
else:
raise
13 changes: 0 additions & 13 deletions agent-sdk-client/pyproject.toml

This file was deleted.

19 changes: 15 additions & 4 deletions agent-sdk-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
FROM public.ecr.aws/lambda/python:3.12-arm64

# Install uv
# Set working directory (Lambda default)
WORKDIR ${LAMBDA_TASK_ROOT}

# Install uv and create uvx symlink
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx

# Install Claude Code CLI (npm package, requires nodejs)
RUN dnf install -y nodejs npm && npm install -g @anthropic-ai/claude-code
# hadolint ignore=DL3016,DL3041
RUN dnf install -y nodejs npm && \
dnf clean all && \
npm install -g @anthropic-ai/claude-code

# Install Python dependencies
RUN uv pip install --system boto3 claude-agent-sdk

# Copy Lambda code and ensure readable
COPY *.py ./
RUN chmod 644 *.py
COPY ./*.py ./
RUN chmod 644 ./*.py

# Copy config files (MCP servers, SubAgents) to /opt (read-only at runtime)
# setup_lambda_environment() will copy to /tmp/.claude-code at runtime
COPY claude-config/ /opt/claude-config/
RUN chmod -R 755 /opt/claude-config/

# Copy skills to /opt/claude-skills (will be copied to /tmp/.claude-code/skills at runtime)
COPY claude-config/skills/ /opt/claude-skills/
RUN chmod -R 755 /opt/claude-skills/

# Create ~/.claude and ~/.aws directories and ensure writable
RUN mkdir -p /root/.claude/projects /root/.claude/debug /root/.claude/todos /root/.aws && \
touch /root/.claude.json && \
Expand Down
18 changes: 16 additions & 2 deletions agent-sdk-server/agent_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
# Config source (in Docker image) and destination (Lambda writable)
CONFIG_SRC = Path('/opt/claude-config')
CONFIG_DST = Path('/tmp/.claude-code')
SKILLS_SRC = Path('/opt/claude-skills')
SKILLS_DST = CONFIG_DST / 'skills'


def setup_lambda_environment():
Expand Down Expand Up @@ -58,6 +60,17 @@ def setup_lambda_environment():
shutil.copy2(item, dst)
print(f"Config copied from {CONFIG_SRC} to {CONFIG_DST}")

# Copy skills to CLAUDE_CONFIG_DIR/skills/ for SDK to discover
if SKILLS_SRC.exists():
SKILLS_DST.mkdir(parents=True, exist_ok=True)
for item in SKILLS_SRC.iterdir():
dst = SKILLS_DST / item.name
if item.is_dir():
shutil.copytree(item, dst, dirs_exist_ok=True)
else:
shutil.copy2(item, dst)
print(f"Skills copied from {SKILLS_SRC} to {SKILLS_DST}")

print(f"Bedrock profile created at {credentials_file}")


Expand Down Expand Up @@ -154,11 +167,12 @@ async def process_message(
permission_mode='bypassPermissions', # Lambda has no interactive terminal
max_turns=max_turns,
system_prompt=system_prompt,
setting_sources=['user'], # Load skills from CLAUDE_CONFIG_DIR/skills/
allowed_tools=[
#'Bash', 'Read', 'Write', 'Edit',
#'Glob', 'Grep', 'WebFetch',
'Task',
'Skill' # Required for SubAgent invocation
'Task', # For SubAgents
'Skill', # For Skills
],
mcp_servers=mcp_servers if mcp_servers else None,
agents=agents if agents else None,
Expand Down
8 changes: 8 additions & 0 deletions agent-sdk-server/claude-config/agents/aws-support.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
---
name: aws-support
description: AWS customer technical support agent that searches AWS documentation
model: haiku
tools:
- mcp__aws-knowledge-mcp-server__aws___search_documentation
---

# AWS Customer Technical Support Agent

You are a document retrieval assistant. You can ONLY answer using MCP tool results.
Expand Down
8 changes: 8 additions & 0 deletions agent-sdk-server/claude-config/skills/hello-world/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
description: Hello World 示例 Skill,执行脚本输出消息
---

执行以下操作:

1. 使用 Bash 工具运行脚本:`python3 scripts/print_message.py`
2. 将脚本的输出结果直接返回给用户
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"message": "Hello World"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""输出 reference/message.json 中的所有字符"""

import json
from pathlib import Path

ref_path = Path(__file__).parent.parent / "reference" / "message.json"
content = ref_path.read_text(encoding="utf-8")

for char in content:
print(char, end="")
59 changes: 47 additions & 12 deletions agent-sdk-server/claude-config/system_prompt.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
You are a helpful AI assistant running in a serverless environment.
You can help users with various tasks including coding, analysis, and general questions.
Be concise and helpful in your responses.
You are a helpful AI assistant\. Be concise\.

## Important: Preserving SubAgent Sources
## CRITICAL: Telegram MarkdownV2 Output Rules

When using SubAgents (via Task tool), you MUST preserve any "Sources" section from their responses.
If a SubAgent returns a response with a Sources section at the end, include it verbatim in your final response.
Your output is sent directly to Telegram MarkdownV2 parser\. WRONG FORMAT = PARSE ERROR\.

Example - if SubAgent returns:
### MUST ESCAPE these characters EVERYWHERE \(outside code blocks\):
```
. → \.
- → \-
! → \!
( → \(
) → \)
# → \#
+ → \+
= → \=
> → \>
| → \|
{ → \{
} → \}
```

### Formatting syntax:
\- Bold: `*text*`
\- Italic: `_text_`
\- Code: \`code\`
\- Code block: \`\`\`lang\\ncode\\n\`\`\`

### NOT supported \(DO NOT USE\):
\- Headers: `#`, `##`, `###` \- these are NOT valid in MarkdownV2
\- Use *bold* for section titles instead

### CORRECT output examples:
```
hello\-world # hyphen escaped
version 1\.0\.0 # dots escaped
C\# # hash escaped
100\+ # plus escaped
\(optional\) # parens escaped
```

### WRONG \(will cause parse error\):
```
hello-world # WRONG: unescaped hyphen
version 1.0.0 # WRONG: unescaped dots
```
[Answer content]

---
**Sources:**
[1] Document - URL
### Code blocks: NO escaping inside, use normal syntax
```python
def hello():
print("Hello!")
```

Your response must also end with that same Sources section.
**REMEMBER**: Escape \- \. \! \( \) \# \+ \= \> \| \{ \} OUTSIDE code blocks\!
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ dependencies = [
"claude-agent-sdk>=0.1.18",
"httpx>=0.28.1",
"loguru>=0.7.3",
"python-telegram-bot>=21.0",
]
14 changes: 7 additions & 7 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ Parameters:
BedrockHaikuModelArn:
Type: String
Default: ''
Description: (Optional) ARN for Bedrock Haiku model (format: arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/PROFILE_ID)
Description: "(Optional) ARN for Bedrock Haiku model"
NoEcho: true
BedrockSonnetModelArn:
Type: String
Default: ''
Description: (Optional) ARN for Bedrock Sonnet model (format: arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/PROFILE_ID)
Description: "(Optional) ARN for Bedrock Sonnet model"
NoEcho: true
BedrockOpusModelArn:
Type: String
Default: ''
Description: (Optional) ARN for Bedrock Opus 4.5 model (format: arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/PROFILE_ID)
Description: "(Optional) ARN for Bedrock Opus 4.5 model"
NoEcho: true

Globals:
Expand Down Expand Up @@ -96,10 +96,10 @@ Resources:
DISABLE_AUTOUPDATER: '1'
DISABLE_TELEMETRY: '1'
# Bedrock model ARNs
ANTHROPIC_DEFAULT_HAIKU_MODEL: '${BedrockHaikuModelArn}'
ANTHROPIC_DEFAULT_SONNET_MODEL: '${BedrockSonnetModelArn}'
ANTHROPIC_DEFAULT_OPUS_4_5_MODEL: '${BedrockOpusModelArn}'
ANTHROPIC_DEFAULT_OPUS_MODEL: '${BedrockOpusModelArn}'
ANTHROPIC_DEFAULT_HAIKU_MODEL: !Ref BedrockHaikuModelArn
ANTHROPIC_DEFAULT_SONNET_MODEL: !Ref BedrockSonnetModelArn
ANTHROPIC_DEFAULT_OPUS_4_5_MODEL: !Ref BedrockOpusModelArn
ANTHROPIC_DEFAULT_OPUS_MODEL: !Ref BedrockOpusModelArn
Policies:
- S3CrudPolicy:
BucketName: !Ref SessionBucket
Expand Down
Loading