diff --git a/.github/workflows/docker-validate.yml b/.github/workflows/docker-validate.yml index 62b66c0..4aef80d 100644 --- a/.github/workflows/docker-validate.yml +++ b/.github/workflows/docker-validate.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 39146d0..8968651 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,4 @@ - Subagent的"tools"字段定义不支持通配符,而是需要具体的Tools名称才可以. +- template.yaml中引用Parameter时必须使用`!Ref`而非字面字符串`'${ParamName}'`. +- agents/*.md文件必须包含YAML frontmatter并定义`name`字段,否则SDK会跳过加载. +- Lambda容器中uvx不可用,需要在Dockerfile中创建符号链接或使用uv安装脚本. diff --git a/README.md b/README.md index 2dc0903..838a127 100644 --- a/README.md +++ b/README.md @@ -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 生命周期管理 @@ -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部署) @@ -121,7 +122,6 @@ sam deploy --guided ## TODO -- [ ] 实现 Skills 支持(参考 `docs/anthropic-agent-sdk-official/skills-in-sdk.md`) - [ ] 多租户 TenantID 隔离 ## License diff --git a/agent-sdk-client/handler.py b/agent-sdk-client/handler.py index f4ffc5c..0d17fe0 100644 --- a/agent-sdk-client/handler.py +++ b/agent-sdk-client/handler.py @@ -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 @@ -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 diff --git a/agent-sdk-client/pyproject.toml b/agent-sdk-client/pyproject.toml deleted file mode 100644 index 84acf92..0000000 --- a/agent-sdk-client/pyproject.toml +++ /dev/null @@ -1,13 +0,0 @@ -[project] -name = "agent-sdk-client" -version = "0.1.0" -description = "Telegram webhook handler for Claude Agent" -requires-python = ">=3.12" -dependencies = [ - "python-telegram-bot>=21.0", - "httpx>=0.27.0", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/agent-sdk-server/Dockerfile b/agent-sdk-server/Dockerfile index 5882951..43ccd94 100644 --- a/agent-sdk-server/Dockerfile +++ b/agent-sdk-server/Dockerfile @@ -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 && \ diff --git a/agent-sdk-server/agent_session.py b/agent-sdk-server/agent_session.py index e7ddf3b..4931446 100644 --- a/agent-sdk-server/agent_session.py +++ b/agent-sdk-server/agent_session.py @@ -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(): @@ -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}") @@ -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, diff --git a/agent-sdk-server/claude-config/agents/aws-support.md b/agent-sdk-server/claude-config/agents/aws-support.md index 6b092a1..085953c 100644 --- a/agent-sdk-server/claude-config/agents/aws-support.md +++ b/agent-sdk-server/claude-config/agents/aws-support.md @@ -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. diff --git a/agent-sdk-server/claude-config/skills/hello-world/SKILL.md b/agent-sdk-server/claude-config/skills/hello-world/SKILL.md new file mode 100644 index 0000000..de29962 --- /dev/null +++ b/agent-sdk-server/claude-config/skills/hello-world/SKILL.md @@ -0,0 +1,8 @@ +--- +description: Hello World 示例 Skill,执行脚本输出消息 +--- + +执行以下操作: + +1. 使用 Bash 工具运行脚本:`python3 scripts/print_message.py` +2. 将脚本的输出结果直接返回给用户 diff --git a/agent-sdk-server/claude-config/skills/hello-world/reference/message.json b/agent-sdk-server/claude-config/skills/hello-world/reference/message.json new file mode 100644 index 0000000..5ea152e --- /dev/null +++ b/agent-sdk-server/claude-config/skills/hello-world/reference/message.json @@ -0,0 +1,3 @@ +{ + "message": "Hello World" +} diff --git a/agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py b/agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py new file mode 100644 index 0000000..399d3fc --- /dev/null +++ b/agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py @@ -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="") diff --git a/agent-sdk-server/claude-config/system_prompt.md b/agent-sdk-server/claude-config/system_prompt.md index d30e19f..2b34408 100644 --- a/agent-sdk-server/claude-config/system_prompt.md +++ b/agent-sdk-server/claude-config/system_prompt.md @@ -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\! diff --git a/pyproject.toml b/pyproject.toml index abe47f5..194cf28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,4 +9,5 @@ dependencies = [ "claude-agent-sdk>=0.1.18", "httpx>=0.28.1", "loguru>=0.7.3", + "python-telegram-bot>=21.0", ] diff --git a/template.yaml b/template.yaml index c8367a5..d8fb472 100644 --- a/template.yaml +++ b/template.yaml @@ -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: @@ -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 diff --git a/tests/test_skills_location.py b/tests/test_skills_location.py new file mode 100644 index 0000000..2f0f9d3 --- /dev/null +++ b/tests/test_skills_location.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""测试 Claude Agent SDK Skills 加载位置""" + +import asyncio +import os +import tempfile +from pathlib import Path + +# 创建测试目录结构 +def setup_test_dirs(): + """创建不同位置的 skill 测试目录""" + # 1. Project skills: {cwd}/.claude/skills/ + # 2. User skills: ~/.claude/skills/ + # 3. CLAUDE_CONFIG_DIR 相关位置 + + test_base = Path(tempfile.mkdtemp(prefix="skill_test_")) + + # 模拟不同的目录结构 + locations = { + "project_cwd": test_base / "workspace", # cwd + "config_dir": test_base / "claude-code", # CLAUDE_CONFIG_DIR + } + + # 创建 skill 在不同位置 + skill_content = """--- +description: Test skill for location verification +--- + +# Test Skill + +This is a test skill to verify loading location. +""" + + # Project skills: {cwd}/.claude/skills/test-skill/SKILL.md + project_skill_dir = locations["project_cwd"] / ".claude" / "skills" / "test-skill" + project_skill_dir.mkdir(parents=True, exist_ok=True) + (project_skill_dir / "SKILL.md").write_text(skill_content) + + # Config dir skills: {CLAUDE_CONFIG_DIR}/skills/test-skill/SKILL.md + config_skill_dir = locations["config_dir"] / "skills" / "test-skill" + config_skill_dir.mkdir(parents=True, exist_ok=True) + (config_skill_dir / "SKILL.md").write_text(skill_content) + + # Config dir .claude/skills: {CLAUDE_CONFIG_DIR}/.claude/skills/test-skill/SKILL.md + config_claude_skill_dir = locations["config_dir"] / ".claude" / "skills" / "test-skill" + config_claude_skill_dir.mkdir(parents=True, exist_ok=True) + (config_claude_skill_dir / "SKILL.md").write_text(skill_content) + + print(f"Test base: {test_base}") + print(f"Project cwd: {locations['project_cwd']}") + print(f"Config dir: {locations['config_dir']}") + print(f"\nCreated skills at:") + print(f" 1. {project_skill_dir}/SKILL.md") + print(f" 2. {config_skill_dir}/SKILL.md") + print(f" 3. {config_claude_skill_dir}/SKILL.md") + + return locations + + +async def test_skill_loading(cwd: str, config_dir: str = None): + """测试不同配置下 skill 是否被加载""" + from claude_agent_sdk import query, ClaudeAgentOptions + + print(f"\n{'='*60}") + print(f"Testing with:") + print(f" cwd: {cwd}") + print(f" CLAUDE_CONFIG_DIR: {config_dir or 'not set'}") + + # 设置环境变量 + if config_dir: + os.environ['CLAUDE_CONFIG_DIR'] = config_dir + elif 'CLAUDE_CONFIG_DIR' in os.environ: + del os.environ['CLAUDE_CONFIG_DIR'] + + options = ClaudeAgentOptions( + cwd=cwd, + setting_sources=["user", "project"], + allowed_tools=["Skill"], + model="haiku", + max_turns=1, + ) + + try: + print(f"\nQuerying: 'What skills are available?'") + async for message in query( + prompt="What skills are available? Just list them briefly.", + options=options + ): + print(f" Response type: {type(message).__name__}") + if hasattr(message, 'content'): + for block in message.content: + if hasattr(block, 'text'): + print(f" Text: {block.text[:200]}...") + except Exception as e: + print(f" Error: {e}") + + +async def main(): + locations = setup_test_dirs() + + # 测试 1: cwd 指向 workspace,不设置 CLAUDE_CONFIG_DIR + await test_skill_loading( + cwd=str(locations["project_cwd"]), + config_dir=None + ) + + # 测试 2: cwd 指向 workspace,CLAUDE_CONFIG_DIR 指向 config_dir + await test_skill_loading( + cwd=str(locations["project_cwd"]), + config_dir=str(locations["config_dir"]) + ) + + # 测试 3: cwd 指向 config_dir + await test_skill_loading( + cwd=str(locations["config_dir"]), + config_dir=str(locations["config_dir"]) + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/test_uvx_in_docker.py b/tests/test_uvx_in_docker.py new file mode 100644 index 0000000..336a2a4 --- /dev/null +++ b/tests/test_uvx_in_docker.py @@ -0,0 +1,201 @@ +"""Test uvx availability in AWS Lambda Python 3.12 ARM64 container. + +This script tests different methods to make uvx command available: +1. Symlink: ln -s /usr/local/bin/uv /usr/local/bin/uvx +2. curl install: curl -LsSf https://astral.sh/uv/install.sh | sh +3. COPY both uv and uvx from official image +""" + +import subprocess +import tempfile +from pathlib import Path +import sys + + +def create_test_dockerfile(method: str) -> str: + """Create a Dockerfile for testing specific uvx installation method.""" + + base = """FROM public.ecr.aws/lambda/python:3.12-arm64 + +# Test different uvx installation methods +""" + + methods = { + "symlink": """# Method 1: Copy uv and create symlink to uvx +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx +""", + + "curl": """# Method 2: Use official install script (installs both uv and uvx) +# Install tar and gzip (required by install script) +RUN dnf install -y tar gzip +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.cargo/bin:${PATH}" +""", + + "copy_both_fallback": """# Method 3: Try to copy uvx, fallback to symlink +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN if [ -f /uvx ]; then cp /uvx /usr/local/bin/uvx; else ln -s /usr/local/bin/uv /usr/local/bin/uvx; fi +""", + } + + test_cmd = """ +# Verify installation +RUN ls -la /usr/local/bin/uv* && uv --version +RUN ls -la /usr/local/bin/uvx && uvx --version + +# Test uvx can execute help +RUN uvx --help + +CMD ["/bin/bash"] +""" + + return base + methods[method] + test_cmd + + +def test_uvx_method(method: str) -> tuple[bool, str, str]: + """Test a specific uvx installation method. + + Returns: + (success, stdout, stderr) + """ + print(f"\n{'='*60}") + print(f"Testing method: {method}") + print('='*60) + + with tempfile.TemporaryDirectory() as tmpdir: + tmppath = Path(tmpdir) + dockerfile = tmppath / "Dockerfile" + + # Create Dockerfile + content = create_test_dockerfile(method) + dockerfile.write_text(content) + print(f"\nDockerfile content:\n{content}") + + # Build image + image_tag = f"test-uvx-{method}:latest" + print(f"\nBuilding image: {image_tag}") + + try: + # Build with progress output + result = subprocess.run( + ["docker", "build", "-t", image_tag, "-f", str(dockerfile), "."], + cwd=tmpdir, + capture_output=True, + text=True, + timeout=300 + ) + + stdout = result.stdout + stderr = result.stderr + + if result.returncode != 0: + print(f"❌ Build failed!") + print(f"STDOUT:\n{stdout}") + print(f"STDERR:\n{stderr}") + return False, stdout, stderr + + print(f"✅ Build successful!") + + # Test uvx command in the container (override Lambda entrypoint) + print(f"\nTesting uvx command execution...") + test_result = subprocess.run( + ["docker", "run", "--rm", "--entrypoint", "/bin/bash", + image_tag, "-c", + "uvx --version && echo '---' && uvx --help"], + capture_output=True, + text=True, + timeout=60 + ) + + print(f"Command output:\n{test_result.stdout}") + + if test_result.returncode != 0: + print(f"❌ uvx execution failed!") + print(f"STDERR:\n{test_result.stderr}") + return False, stdout + "\n" + test_result.stdout, stderr + "\n" + test_result.stderr + + print(f"✅ uvx command works!") + + # Cleanup image + subprocess.run(["docker", "rmi", image_tag], + capture_output=True, timeout=30) + + return True, stdout + "\n" + test_result.stdout, stderr + + except subprocess.TimeoutExpired: + print(f"❌ Timeout!") + return False, "", "Timeout during build or test" + except Exception as e: + print(f"❌ Error: {e}") + return False, "", str(e) + + +def main(): + """Run all tests and report results.""" + print("="*60) + print("Testing uvx availability in AWS Lambda container") + print("="*60) + + methods = ["symlink", "curl", "copy_both_fallback"] + results = {} + + for method in methods: + success, stdout, stderr = test_uvx_method(method) + results[method] = { + "success": success, + "stdout": stdout, + "stderr": stderr + } + + # Print summary + print("\n" + "="*60) + print("SUMMARY") + print("="*60) + + for method, result in results.items(): + status = "✅ SUCCESS" if result["success"] else "❌ FAILED" + print(f"{method:15s}: {status}") + + # Print recommendation + print("\n" + "="*60) + print("RECOMMENDATION") + print("="*60) + + successful_methods = [m for m, r in results.items() if r["success"]] + + if not successful_methods: + print("❌ No method succeeded. Check Docker availability and network.") + return 1 + + # Prefer symlink (simplest and fastest) + if "symlink" in successful_methods: + print("""✅ Use symlink method (simplest and fastest): + +Add to Dockerfile after copying uv: + RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx + +Complete lines: + COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx +""") + elif "curl" in successful_methods: + print("""✅ Use curl install method: + +Replace current uv installation with: + RUN curl -LsSf https://astral.sh/uv/install.sh | sh + ENV PATH="/root/.cargo/bin:${PATH}" +""") + elif "copy_both_fallback" in successful_methods: + print("""✅ Use copy_both_fallback method: + +Replace current uv installation with: + COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + RUN if [ -f /uvx ]; then cp /uvx /usr/local/bin/uvx; else ln -s /usr/local/bin/uv /usr/local/bin/uvx; fi +""") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/uv.lock b/uv.lock index f706190..05f5f90 100644 --- a/uv.lock +++ b/uv.lock @@ -358,6 +358,7 @@ dependencies = [ { name = "claude-agent-sdk" }, { name = "httpx" }, { name = "loguru" }, + { name = "python-telegram-bot" }, ] [package.metadata] @@ -366,6 +367,7 @@ requires-dist = [ { name = "claude-agent-sdk", specifier = ">=0.1.18" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "python-telegram-bot", specifier = ">=21.0" }, ] [[package]] @@ -521,6 +523,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, ] +[[package]] +name = "python-telegram-bot" +version = "22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1c519a3ca8ad0babc650ec63dbfbd1b73babf625ed54/python_telegram_bot-22.5.tar.gz", hash = "sha256:82d4efd891d04132f308f0369f5b5929e0b96957901f58bcef43911c5f6f92f8", size = 1488269, upload-time = "2025-09-27T13:50:27.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/340c7520095a8c79455fcf699cbb207225e5b36490d2b9ee557c16a7b21b/python_telegram_bot-22.5-py3-none-any.whl", hash = "sha256:4b7cd365344a7dce54312cc4520d7fa898b44d1a0e5f8c74b5bd9b540d035d16", size = 730976, upload-time = "2025-09-27T13:50:25.93Z" }, +] + [[package]] name = "pywin32" version = "311"