From 58ed60f7b7dfb9571111b5d7e62ee58736cf7648 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 7 May 2026 23:24:48 -0500 Subject: [PATCH] Add EventBridge scheduling: schedule_reminder, list_reminders, cancel_reminder --- agentclaw/agentcore/agentcore.json | 5 +- agentclaw/app/agent_claw_main/main.py | 13 +- agentclaw/app/agent_claw_main/main.py.bak | 317 ++++++++++++++++++ .../app/agent_claw_main/tools/scheduler.py | 141 ++++++++ ...311bd0bec377fa20fc8a0b4cf4be3998efe0b4.zip | Bin 0 -> 3910 bytes ...7aa12b9b8a02598259b8977593795f6e631143.zip | Bin 0 -> 3278 bytes ...724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa.zip | Bin 0 -> 3753 bytes ...04200dbdffee263d76fca541a8630534d8f5c5.zip | Bin 0 -> 3923 bytes ...e4f550bd570a6e092e76dc5d243e2aba10d018.zip | Bin 0 -> 3756 bytes ...ec5c52b21a60fe8d11b578714edc68a5bd9113.zip | Bin 0 -> 3446 bytes ...291b1d2bcba46ec44c03c1b411b7703af8c47d.zip | Bin 0 -> 3544 bytes ...55b7f1e2c7106e43f2d0ed2772f0a1c63460cc.zip | Bin 0 -> 3467 bytes ...c5ba853252e7157d8d8958ac5fda92e72edb1f.zip | Bin 0 -> 655 bytes ...58f2a5fc66785d31998cfa942892d925fd807e.zip | Bin 0 -> 2848 bytes ...4d3b7b8e09e8116ee642b23ea53ceb1a33d528.zip | Bin 0 -> 3700 bytes ...4495f429b3ccffecd170e6fa5b8c46361af321.zip | Bin 0 -> 3770 bytes cdk/cdk.out/AgentClawStack.assets.json | 29 +- cdk/cdk.out/AgentClawStack.metadata.json | 92 ++++- cdk/cdk.out/AgentClawStack.template.json | 161 ++++++++- .../handler.py | 268 +++++++++++++++ .../requirements.txt | 1 + .../handler.py | 232 +++++++++++++ .../requirements.txt | 1 + .../handler.py | 263 +++++++++++++++ .../requirements.txt | 1 + .../handler.py | 268 +++++++++++++++ .../requirements.txt | 1 + .../handler.py | 260 ++++++++++++++ .../requirements.txt | 1 + .../handler.py | 244 ++++++++++++++ .../requirements.txt | 1 + .../handler.py | 251 ++++++++++++++ .../requirements.txt | 1 + .../handler.py | 246 ++++++++++++++ .../requirements.txt | 1 + .../handler.py | 29 ++ .../handler.py | 196 +++++++++++ .../requirements.txt | 1 + .../handler.py | 261 ++++++++++++++ .../requirements.txt | 1 + .../handler.py | 263 +++++++++++++++ .../requirements.txt | 1 + cdk/cdk.out/manifest.json | 2 +- cdk/cdk.out/tree.json | 2 +- cdk/lib/agent-claw-stack.ts | 46 +++ src/lambdas/scheduler/handler.py | 29 ++ 46 files changed, 3605 insertions(+), 24 deletions(-) create mode 100644 agentclaw/app/agent_claw_main/main.py.bak create mode 100644 agentclaw/app/agent_claw_main/tools/scheduler.py create mode 100644 cdk/cdk.out/.cache/0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4.zip create mode 100644 cdk/cdk.out/.cache/1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143.zip create mode 100644 cdk/cdk.out/.cache/49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa.zip create mode 100644 cdk/cdk.out/.cache/49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5.zip create mode 100644 cdk/cdk.out/.cache/72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018.zip create mode 100644 cdk/cdk.out/.cache/7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113.zip create mode 100644 cdk/cdk.out/.cache/83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d.zip create mode 100644 cdk/cdk.out/.cache/858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc.zip create mode 100644 cdk/cdk.out/.cache/8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip create mode 100644 cdk/cdk.out/.cache/b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e.zip create mode 100644 cdk/cdk.out/.cache/e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528.zip create mode 100644 cdk/cdk.out/.cache/eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321.zip create mode 100644 cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/handler.py create mode 100644 cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/requirements.txt create mode 100644 cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/handler.py create mode 100644 cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/requirements.txt create mode 100644 cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/handler.py create mode 100644 cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/requirements.txt create mode 100644 cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/handler.py create mode 100644 cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/requirements.txt create mode 100644 cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/handler.py create mode 100644 cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/requirements.txt create mode 100644 cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/handler.py create mode 100644 cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/requirements.txt create mode 100644 cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/handler.py create mode 100644 cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/requirements.txt create mode 100644 cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/handler.py create mode 100644 cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/requirements.txt create mode 100644 cdk/cdk.out/asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f/handler.py create mode 100644 cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/handler.py create mode 100644 cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/requirements.txt create mode 100644 cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/handler.py create mode 100644 cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/requirements.txt create mode 100644 cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/handler.py create mode 100644 cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/requirements.txt create mode 100644 src/lambdas/scheduler/handler.py diff --git a/agentclaw/agentcore/agentcore.json b/agentclaw/agentcore/agentcore.json index e38e377..e07916f 100644 --- a/agentclaw/agentcore/agentcore.json +++ b/agentclaw/agentcore/agentcore.json @@ -21,7 +21,8 @@ "USERS_TABLE_NAME": "agent-claw-users", "WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548", "TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3", - "BRAVE_API_KEY_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi" + "BRAVE_API_KEY_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi", + "SCHEDULER_LAMBDA_ARN": "arn:aws:lambda:us-east-1:495395224548:function:agent-claw-scheduler" } } ], @@ -59,4 +60,4 @@ "configBundles": [], "abTests": [], "httpGateways": [] -} \ No newline at end of file +} diff --git a/agentclaw/app/agent_claw_main/main.py b/agentclaw/app/agent_claw_main/main.py index 5dbc6e2..c30926b 100644 --- a/agentclaw/app/agent_claw_main/main.py +++ b/agentclaw/app/agent_claw_main/main.py @@ -13,6 +13,8 @@ from prompt_builder import build_system_prompt, invalidate_prompt from tools import web as web_tools from tools import workspace as ws_tools from tools import messaging +from tools.scheduler import schedule_reminder, list_reminders, cancel_reminder +import tools.scheduler as _scheduler_module from tools.home_assistant import home_assistant, set_ha_config from mcp.client.streamable_http import streamablehttp_client from strands.tools.mcp.mcp_client import MCPClient @@ -178,6 +180,7 @@ def manage_service(action: str, service: str, config: dict | None = None) -> str # Module-level actor_id for tool closures (set per-invocation) _current_actor_id: str = '' +_current_chat_id: str = '' @app.entrypoint @@ -217,6 +220,10 @@ async def main(payload: dict, context): actor_id = payload.get('actor_id', adapter_config.get('target_id', 'default')) session_id = payload.get('session_id', f'session-{actor_id}') _current_actor_id = actor_id + chat_id = adapter_config.get('target_id', '') + _current_chat_id = chat_id + _scheduler_module._current_actor_id = actor_id + _scheduler_module._current_chat_id = chat_id memory_config = AgentCoreMemoryConfig( memory_id=MEMORY_ID, @@ -258,7 +265,9 @@ async def main(payload: dict, context): from zoneinfo import ZoneInfo _tz = ZoneInfo('America/Chicago') _now = datetime.now(_tz) - system_prompt += f'\n\nCurrent date/time: {_now.strftime("%A, %B %d, %Y %I:%M %p %Z")}' + _time_str = _now.strftime('%A, %B %d, %Y %I:%M %p %Z') + system_prompt = system_prompt + f'\n\nCurrent date/time: {_time_str}' + print(f'[main] System prompt time injection: {_time_str}') # Model: claude-sonnet-4-6 via cross-region inference with extended thinking from botocore.config import Config as BotoConfig @@ -273,7 +282,7 @@ async def main(payload: dict, context): base_tools = [web_search, web_fetch, read_workspace_file, write_workspace_file, _code_interpreter.code_interpreter, home_assistant, connect_google_account, - manage_service] + manage_service, schedule_reminder, list_reminders, cancel_reminder] workspace_mcp_client = MCPClient( lambda: streamablehttp_client(WORKSPACE_MCP_URL, timeout=20, auth=_SigV4HttpxAuth(actor_id=actor_id)) diff --git a/agentclaw/app/agent_claw_main/main.py.bak b/agentclaw/app/agent_claw_main/main.py.bak new file mode 100644 index 0000000..cd17a12 --- /dev/null +++ b/agentclaw/app/agent_claw_main/main.py.bak @@ -0,0 +1,317 @@ +""" +agent-claw Runtime 1 — main assistant agent. + +Entrypoint for AgentCore CodeZip deployment. +""" +import os +from strands import Agent, tool +from strands.models import BedrockModel +from bedrock_agentcore.runtime import BedrockAgentCoreApp + +from channels.telegram import TelegramAdapter +from prompt_builder import build_system_prompt, invalidate_prompt +from tools import web as web_tools +from tools import workspace as ws_tools +from tools import messaging +from tools.home_assistant import home_assistant, set_ha_config +from mcp.client.streamable_http import streamablehttp_client +from strands.tools.mcp.mcp_client import MCPClient +import httpx +import botocore.auth +import botocore.awsrequest +import boto3 +from urllib.parse import urlparse as _urlparse + +WORKSPACE_MCP_URL = 'https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp' +OAUTH_START_URL = os.environ.get('OAUTH_START_URL', '') +USERS_TABLE_NAME = os.environ.get('USERS_TABLE_NAME', 'agent-claw-users') + + +class _SigV4HttpxAuth(httpx.Auth): + """SigV4 auth for Lambda Function URL with AWS_IAM, plus X-Actor-Id header.""" + def __init__(self, region: str = 'us-east-1', actor_id: str = ''): + self._region = region + self._actor_id = actor_id + + def auth_flow(self, request): + creds = boto3.Session().get_credentials().get_frozen_credentials() + parsed = _urlparse(str(request.url)) + aws_req = botocore.awsrequest.AWSRequest( + method=request.method, + url=str(request.url), + data=request.content or b'', + headers={ + 'Host': parsed.hostname, + 'Content-Type': request.headers.get('content-type', 'application/json'), + 'Accept': request.headers.get('accept', 'application/json, text/event-stream'), + } + ) + botocore.auth.SigV4Auth(creds, 'lambda', self._region).add_auth(aws_req) + for k, v in aws_req.headers.items(): + request.headers[k] = v + if self._actor_id: + request.headers['x-actor-id'] = self._actor_id + yield request +from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig +from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager +from strands_tools.code_interpreter import AgentCoreCodeInterpreter as _CodeInterpreterClient + +# Initialise once per warm session +_code_interpreter = _CodeInterpreterClient(region='us-east-1') + +app = BedrockAgentCoreApp() + + +# ── Tool definitions ────────────────────────────────────────────────────── + +@tool +def send_message(text: str) -> str: + """Send a message to the user. Use multiple calls to send incrementally - send the direct answer first, then elaboration. Each call delivers immediately to the user.""" + return messaging.send(text) + + +@tool +def web_search(query: str) -> str: + """Search the web using Brave Search. Returns titles, URLs, and snippets.""" + return web_tools.brave_search(query) + + +@tool +def web_fetch(url: str) -> str: + """Fetch and extract readable text content from a URL.""" + return web_tools.web_fetch(url) + + +@tool +def read_workspace_file(path: str) -> str: + """Read a file from the agent workspace (SOUL.md, HEARTBEAT.md, etc.)""" + return ws_tools.read_file(path) + + +@tool +def write_workspace_file(path: str, content: str) -> str: + """Write or update a file in the agent workspace.""" + result = ws_tools.write_file(path, content) + invalidate_prompt() # force system prompt rebuild if persona files changed + return result + + +@tool +def connect_google_account() -> str: + """Generate a Google OAuth authorization URL for the current user to connect their Google account. + Use this when the user wants to connect Google Workspace (Gmail, Calendar, Drive, etc.) + or when Google tools fail due to missing credentials.""" + if not OAUTH_START_URL: + return 'Google OAuth is not configured. Set OAUTH_START_URL environment variable.' + actor_id = _current_actor_id + if not actor_id: + return 'Cannot determine actor_id for OAuth flow.' + url = f'{OAUTH_START_URL}?actor_id={actor_id}' + return f'Please open this URL to connect your Google account:\n{url}\n\nAfter authorizing, Google Workspace tools (Gmail, Calendar, Drive) will be available.' + + +@tool +def manage_service(action: str, service: str, config: dict | None = None) -> str: + """Enroll, update, remove, or list external services for your account. + + Actions: + - "enroll": Add or update a service (requires service name and config dict). + - "remove": Remove a service by name. + - "list": List all enrolled services (shows service names, not secrets). + + Supported services: + - "home_assistant": config = {"url": "https://your-ha-url", "token": "long-lived-access-token"} + + Examples: + - Enroll HA: manage_service(action="enroll", service="home_assistant", + config={"url": "https://ha.example.com", "token": "eyJ..."}) + - Remove HA: manage_service(action="remove", service="home_assistant") + - List all: manage_service(action="list") + """ + actor_id = _current_actor_id + if not actor_id: + return 'Cannot determine actor_id.' + + ddb = boto3.resource('dynamodb', region_name='us-east-1') + table = ddb.Table(USERS_TABLE_NAME) + + if action == 'list': + resp = table.get_item(Key={'actor_id': actor_id}) + services = resp.get('Item', {}).get('services', {}) + if not services: + return 'No services enrolled.' + lines = [f"- {svc}: configured" for svc in services] + return 'Enrolled services:\n' + '\n'.join(lines) + + elif action == 'enroll': + if not service: + return 'service name is required.' + if not config: + return 'config dict is required for enroll.' + # Validate known services + if service == 'home_assistant': + if 'url' not in config or 'token' not in config: + return 'home_assistant config requires "url" and "token" keys.' + # Update in-memory config immediately for this session + set_ha_config(config['url'], config['token']) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET services = if_not_exists(services, :empty), services.#svc = :cfg', + ExpressionAttributeNames={'#svc': service}, + ExpressionAttributeValues={':cfg': config, ':empty': {}}, + ) + return f'Service "{service}" enrolled successfully.' + + elif action == 'remove': + if not service: + return 'service name is required.' + if service == 'home_assistant': + set_ha_config('', '') + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='REMOVE services.#svc', + ExpressionAttributeNames={'#svc': service}, + ) + return f'Service "{service}" removed.' + + else: + return f'Unknown action: {action}. Use "enroll", "remove", or "list".' + + +# ── Entrypoint ──────────────────────────────────────────────────────────── + +# Module-level actor_id for tool closures (set per-invocation) +_current_actor_id: str = '' + + +@app.entrypoint +def main(payload: dict, context) -> dict: + """Handle an invocation from agent-runner Lambda.""" + global _current_actor_id + + # Set up channel adapter + adapter_config = payload.get('channel_adapter', {}) + channel_type = adapter_config.get('type', 'telegram') + + if channel_type == 'telegram': + adapter = TelegramAdapter( + chat_id=adapter_config.get('target_id', ''), + bot_token_secret_arn=adapter_config.get('bot_token_secret_arn', ''), + ) + else: + raise ValueError(f"Unsupported channel type: {channel_type}") + + messaging.set_adapter(adapter) + + # Start typing indicator immediately, keep it alive in background + import threading + _typing_active = True + def _keep_typing(): + adapter.send_typing() + import time + while _typing_active: + time.sleep(4) + if _typing_active: + adapter.send_typing() + typing_thread = threading.Thread(target=_keep_typing, daemon=True) + typing_thread.start() + + # Set up AgentCore Memory session manager (short + long term via session_manager) + MEMORY_ID = 'agentclaw_AgentClawMemory-i7Csf776AH' + actor_id = payload.get('actor_id', adapter_config.get('target_id', 'default')) + session_id = payload.get('session_id', f'session-{actor_id}') + _current_actor_id = actor_id + + memory_config = AgentCoreMemoryConfig( + memory_id=MEMORY_ID, + session_id=session_id, + actor_id=actor_id, + ) + session_manager = AgentCoreMemorySessionManager( + agentcore_memory_config=memory_config, + region_name='us-east-1', + ) + + # Inject per-user service configs + user_profile = payload.get('user_profile', {}) + services = user_profile.get('services', {}) + + ha_cfg = services.get('home_assistant', {}) + set_ha_config(ha_cfg.get('url', ''), ha_cfg.get('token', '')) + + # Build system prompt — base cached, user context injected per-invocation + user_context = '' + if user_profile: + name = user_profile.get('display_name', '') + username = user_profile.get('telegram_username', '') + google_email = user_profile.get('google_email', '') + user_context = f'Name: {name}' + if username: + user_context += f'\nTelegram username: @{username}' + if google_email: + user_context += f'\nGoogle account: {google_email}' + else: + user_context += '\nGoogle account: not connected (use connect_google_account tool to connect)' + enrolled = list(services.keys()) + if enrolled: + user_context += f'\nEnrolled services: {", ".join(enrolled)}' + system_prompt = build_system_prompt(user_context=user_context, actor_id=actor_id) + + # Model: claude-sonnet-4-6 via cross-region inference + model = BedrockModel( + model_id="us.anthropic.claude-sonnet-4-6", + region_name="us-east-1", + ) + + base_tools = [send_message, web_search, web_fetch, read_workspace_file, write_workspace_file, + _code_interpreter.code_interpreter, home_assistant, connect_google_account, + manage_service] + + def _run_agent(tools): + agent = Agent( + model=model, + system_prompt=system_prompt, + session_manager=session_manager, + tools=tools, + ) + return agent(payload.get('prompt', '')) + + workspace_mcp_client = MCPClient( + lambda: streamablehttp_client(WORKSPACE_MCP_URL, timeout=20, auth=_SigV4HttpxAuth(actor_id=actor_id)) + ) + workspace_tools = [] + google_email = user_profile.get('google_email', '') + if google_email: + try: + with workspace_mcp_client: + workspace_tools = workspace_mcp_client.list_tools_sync() + except Exception as e: + print(f'[main] workspace_mcp unavailable ({type(e).__name__}) — continuing without it') + else: + print(f'[main] actor={actor_id} has no google_email — skipping workspace_mcp') + + try: + result = _run_agent(base_tools + list(workspace_tools)) + finally: + _typing_active = False + + # Flush buffered memory events + session_manager.close() + + # Deliver final response + if not messaging.was_sent() and result.message: + msg = result.message + if isinstance(msg, dict): + content = msg.get('content', {}) + if isinstance(content, dict): + msg = content.get('text', str(content)) + elif isinstance(content, list): + msg = ' '.join(c.get('text', '') for c in content if isinstance(c, dict)) + else: + msg = str(content) + adapter.send(str(msg)) + + return {'result': result.message} + + +app.run() diff --git a/agentclaw/app/agent_claw_main/tools/scheduler.py b/agentclaw/app/agent_claw_main/tools/scheduler.py new file mode 100644 index 0000000..e060c80 --- /dev/null +++ b/agentclaw/app/agent_claw_main/tools/scheduler.py @@ -0,0 +1,141 @@ +"""EventBridge scheduling tools: schedule_reminder, list_reminders, cancel_reminder.""" +import json +import os +import re +import boto3 +from strands import tool + +# Injected by main.py before each invocation +_current_actor_id: str = '' +_current_chat_id: str = '' + +SCHEDULER_LAMBDA_ARN = os.environ.get('SCHEDULER_LAMBDA_ARN', '') +ACCOUNT_ID = os.environ.get('AWS_ACCOUNT_ID', '') +REGION = 'us-east-1' + + +def _eb(): + return boto3.client('events', region_name=REGION) + + +def _rule_prefix() -> str: + safe = re.sub(r'[^a-zA-Z0-9_-]', '-', _current_actor_id) + return f'agent-claw-reminder-{safe}-' + + +@tool +def schedule_reminder(message: str, when_utc: str) -> str: + """Schedule a one-time reminder to be sent via Telegram at a specific UTC time. + + Args: + message: The reminder text to send. + when_utc: ISO 8601 UTC datetime, e.g. '2026-05-09T09:00:00' (no timezone suffix). + """ + if not SCHEDULER_LAMBDA_ARN: + return 'SCHEDULER_LAMBDA_ARN not configured.' + if not _current_chat_id: + return 'chat_id not available.' + + # Convert ISO datetime to EventBridge cron: cron(min hour day month ? year) + try: + from datetime import datetime + dt = datetime.fromisoformat(when_utc.rstrip('Z')) + cron_expr = f'cron({dt.minute} {dt.hour} {dt.day} {dt.month} ? {dt.year})' + except ValueError as e: + return f'Invalid when_utc format: {e}' + + import time + rule_name = f'{_rule_prefix()}{int(time.time())}' + + eb = _eb() + eb.put_rule( + Name=rule_name, + ScheduleExpression=cron_expr, + State='ENABLED', + ) + + # Grant EventBridge permission to invoke the Lambda + lm = boto3.client('lambda', region_name=REGION) + try: + lm.add_permission( + FunctionName=SCHEDULER_LAMBDA_ARN, + StatementId=rule_name, + Action='lambda:InvokeFunction', + Principal='events.amazonaws.com', + SourceArn=f'arn:aws:events:{REGION}:{_account_id()}:rule/{rule_name}', + ) + except lm.exceptions.ResourceConflictException: + pass + + eb.put_targets( + Rule=rule_name, + Targets=[{ + 'Id': 'scheduler', + 'Arn': SCHEDULER_LAMBDA_ARN, + 'Input': json.dumps({ + 'chat_id': _current_chat_id, + 'message': message, + 'rule_name': rule_name, + }), + }], + ) + + return f'Reminder scheduled: "{message}" at {when_utc} UTC (rule: {rule_name})' + + +@tool +def list_reminders() -> str: + """List all pending reminders for the current user.""" + eb = _eb() + prefix = _rule_prefix() + rules = [] + kwargs: dict = {'NamePrefix': prefix} + while True: + resp = eb.list_rules(**kwargs) + rules.extend(resp.get('Rules', [])) + token = resp.get('NextToken') + if not token: + break + kwargs['NextToken'] = token + + if not rules: + return 'No pending reminders.' + + lines = [] + for r in rules: + lines.append(f"- {r['Name']}: {r.get('ScheduleExpression', '')} [{r.get('State', '')}]") + return '\n'.join(lines) + + +@tool +def cancel_reminder(rule_name: str) -> str: + """Cancel a scheduled reminder by its rule name. + + Args: + rule_name: The EventBridge rule name (from list_reminders). + """ + prefix = _rule_prefix() + if not rule_name.startswith(prefix): + return f'Rule "{rule_name}" does not belong to your account.' + + eb = _eb() + try: + eb.remove_targets(Rule=rule_name, Ids=['scheduler']) + eb.delete_rule(Name=rule_name) + except eb.exceptions.ResourceNotFoundException: + return f'Rule "{rule_name}" not found.' + + # Remove Lambda permission + lm = boto3.client('lambda', region_name=REGION) + try: + lm.remove_permission(FunctionName=SCHEDULER_LAMBDA_ARN, StatementId=rule_name) + except Exception: + pass + + return f'Reminder "{rule_name}" cancelled.' + + +def _account_id() -> str: + if ACCOUNT_ID: + return ACCOUNT_ID + return boto3.client('sts', region_name=REGION).get_caller_identity()['Account'] diff --git a/cdk/cdk.out/.cache/0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4.zip b/cdk/cdk.out/.cache/0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4.zip new file mode 100644 index 0000000000000000000000000000000000000000..b82f28824653f4cb4d5e460605f2153b3da3dcb7 GIT binary patch literal 3910 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 literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143.zip b/cdk/cdk.out/.cache/1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143.zip new file mode 100644 index 0000000000000000000000000000000000000000..2883e9e7ab41b6f429476c6a882ebf67818b7447 GIT binary patch literal 3278 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 Ii}g?Se?lEJ^Z)<= literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa.zip b/cdk/cdk.out/.cache/49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa.zip new file mode 100644 index 0000000000000000000000000000000000000000..30acb8a3092c4478e84e38126eef7fe1e2de47b8 GIT binary patch literal 3753 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 literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5.zip b/cdk/cdk.out/.cache/49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5.zip new file mode 100644 index 0000000000000000000000000000000000000000..359eb4e3b2e4f1735403e35e3069814f753cc528 GIT binary patch literal 3923 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 literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018.zip b/cdk/cdk.out/.cache/72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018.zip new file mode 100644 index 0000000000000000000000000000000000000000..1fe26cd73735c152ce7ed7e3bff5cfec24d4e8d3 GIT binary patch literal 3756 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 OKlSR*%lS77;Qs)e!sN#Q literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113.zip b/cdk/cdk.out/.cache/7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113.zip new file mode 100644 index 0000000000000000000000000000000000000000..6a115fee573a03d02dbf6e1fcdf12b9bb8c9258b GIT binary patch literal 3446 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 WSD72r(EfE$o>%bsyK(LT0R98-=4lWB literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d.zip b/cdk/cdk.out/.cache/83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d.zip new file mode 100644 index 0000000000000000000000000000000000000000..bb0ae4fe5c22e65cf69d818305c7c6530f5f94cb GIT binary patch literal 3544 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 NC;a|YeTKiU{{YzHszU$( literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc.zip b/cdk/cdk.out/.cache/858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc.zip new file mode 100644 index 0000000000000000000000000000000000000000..dbb6cc55cc4fa3dbf36eabfa143e7951b0d2bae1 GIT binary patch literal 3467 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 literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip b/cdk/cdk.out/.cache/8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip new file mode 100644 index 0000000000000000000000000000000000000000..b1f8e8ec7e4a4f0476ac7f282fc2cc61a6d9a987 GIT binary patch literal 655 zcmWIWW@Zs#-~d7f21Ou704^XiBQY-}C$&hgpt3diblz{FJRN!VUC$+xSh1&tIOQ@Gtc)-w(Mm*wKn_N)TO~OCo1Kbz8UQ}wk&+zgCpI6?|E}4a+mD0*mzuk z%X#zL>h>JQp7ObqOw}{b+VC7d$DDoP_w-BmG;c3FDI=#Qet!QEh2t{O&Bl|eI7&iV zuG&94{^jTYnSF}4Z^>;`c_*bZe{FT^QPbk!3ZZ>B4CA+JRJHcKWj`A6^zi!7w(U2& zUh}W<{3IRV&Cb!EG-(Yd69dC`W(I};Z$>6vW<(M~mIEar2vA^H(g$XUD;r3J O5eO}Sv?I_w1_l7KtrIo? literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e.zip b/cdk/cdk.out/.cache/b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e.zip new file mode 100644 index 0000000000000000000000000000000000000000..285096cdc6ddbbafb43b43bd92ab6bfca2db53b2 GIT binary patch literal 2848 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 literal 0 HcmV?d00001 diff --git a/cdk/cdk.out/.cache/e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528.zip b/cdk/cdk.out/.cache/e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528.zip new file mode 100644 index 0000000000000000000000000000000000000000..33f4da4997e875ae67edb9301a92aba1d1abc0b9 GIT binary patch literal 3700 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 (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -46,7 +46,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:294:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:335:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -60,7 +60,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:298:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:339:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -74,7 +74,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:303:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:344:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -88,7 +88,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:308:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:349:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -102,7 +102,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:313:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:354:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -116,7 +116,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:318:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:359:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -130,7 +130,7 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:323:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:364:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -144,7 +144,21 @@ { "type": "aws:cdk:creationStack", "data": [ - "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:328:5)", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:369:5)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/SchedulerLambdaArn": [ + { + "type": "aws:cdk:logicalId", + "data": "SchedulerLambdaArn" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:374:5)", " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "...node internals, ts-node, ts-node, ts-node..." ] @@ -296,6 +310,36 @@ ] } ], + "/AgentClawStack/Scheduler/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "SchedulerCFE73206" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...new Function2 in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:290:25)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], + "/AgentClawStack/Scheduler/EventBridgeInvoke": [ + { + "type": "aws:cdk:logicalId", + "data": "SchedulerEventBridgeInvoke72A0529A" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...WrappedClass.addPermission in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:303:17)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], "/AgentClawStack/CDKMetadata/Default": [ { "type": "aws:cdk:logicalId", @@ -516,6 +560,21 @@ ] } ], + "/AgentClawStack/Scheduler/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "SchedulerServiceRole62CDA70C" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...new Function2 in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:290:25)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } + ], "/AgentClawStack/TgIngest/ServiceRole/DefaultPolicy/Resource": [ { "type": "aws:cdk:logicalId", @@ -611,5 +670,20 @@ "...node internals, ts-node, ts-node, ts-node..." ] } + ], + "/AgentClawStack/Scheduler/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "SchedulerServiceRoleDefaultPolicyFA0D8235" + }, + { + "type": "aws:cdk:creationStack", + "data": [ + "...environmentFromArn.grantRead in aws-cdk-lib...", + "new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:301:20)", + " (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", + "...node internals, ts-node, ts-node, ts-node..." + ] + } ] } \ No newline at end of file diff --git a/cdk/cdk.out/AgentClawStack.template.json b/cdk/cdk.out/AgentClawStack.template.json index d480952..d9a5f98 100644 --- a/cdk/cdk.out/AgentClawStack.template.json +++ b/cdk/cdk.out/AgentClawStack.template.json @@ -387,7 +387,7 @@ "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", - "S3Key": "c0db2a060be885d61722dfe6fbd3967e1c956826682078f6338123bf0c797e5b.zip" + "S3Key": "49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5.zip" }, "Environment": { "Variables": { @@ -423,7 +423,7 @@ ], "Metadata": { "aws:cdk:path": "AgentClawStack/AgentRunner/Resource", - "aws:asset:path": "asset.c0db2a060be885d61722dfe6fbd3967e1c956826682078f6338123bf0c797e5b", + "aws:asset:path": "asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5", "aws:asset:is-bundled": false, "aws:asset:property": "Code" } @@ -872,6 +872,33 @@ "Effect": "Allow", "Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*", "Sid": "PerUserGoogleCredentialsReadRuntime" + }, + { + "Action": [ + "events:PutRule", + "events:PutTargets", + "events:ListRules", + "events:ListTargetsByRule", + "events:RemoveTargets", + "events:DeleteRule" + ], + "Effect": "Allow", + "Resource": "arn:aws:events:us-east-1:*:rule/agent-claw-reminder-*", + "Sid": "EventBridgeScheduler" + }, + { + "Action": [ + "lambda:AddPermission", + "lambda:RemovePermission" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "SchedulerCFE73206", + "Arn" + ] + }, + "Sid": "SchedulerLambdaPermission" } ], "Version": "2012-10-17" @@ -1114,6 +1141,127 @@ "aws:asset:property": "Code" } }, + "SchedulerServiceRole62CDA70C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/Scheduler/ServiceRole/Resource" + } + }, + "SchedulerServiceRoleDefaultPolicyFA0D8235": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3" + }, + { + "Action": [ + "events:RemoveTargets", + "events:DeleteRule" + ], + "Effect": "Allow", + "Resource": "arn:aws:events:us-east-1:495395224548:rule/agent-claw-reminder-*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SchedulerServiceRoleDefaultPolicyFA0D8235", + "Roles": [ + { + "Ref": "SchedulerServiceRole62CDA70C" + } + ] + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/Scheduler/ServiceRole/DefaultPolicy/Resource" + } + }, + "SchedulerCFE73206": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", + "S3Key": "8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip" + }, + "Environment": { + "Variables": { + "TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3" + } + }, + "FunctionName": "agent-claw-scheduler", + "Handler": "handler.handler", + "MemorySize": 128, + "Role": { + "Fn::GetAtt": [ + "SchedulerServiceRole62CDA70C", + "Arn" + ] + }, + "Runtime": "python3.12", + "Timeout": 30 + }, + "DependsOn": [ + "SchedulerServiceRoleDefaultPolicyFA0D8235", + "SchedulerServiceRole62CDA70C" + ], + "Metadata": { + "aws:cdk:path": "AgentClawStack/Scheduler/Resource", + "aws:asset:path": "asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f", + "aws:asset:is-bundled": false, + "aws:asset:property": "Code" + } + }, + "SchedulerEventBridgeInvoke72A0529A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "SchedulerCFE73206", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": "arn:aws:events:us-east-1:495395224548:rule/agent-claw-reminder-*" + }, + "Metadata": { + "aws:cdk:path": "AgentClawStack/Scheduler/EventBridgeInvoke" + } + }, "CDKMetadata": { "Type": "AWS::CDK::Metadata", "Properties": { @@ -1216,6 +1364,15 @@ "Arn" ] } + }, + "SchedulerLambdaArn": { + "Description": "Scheduler Lambda ARN — set as SCHEDULER_LAMBDA_ARN in agentcore.json", + "Value": { + "Fn::GetAtt": [ + "SchedulerCFE73206", + "Arn" + ] + } } }, "Parameters": { diff --git a/cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/handler.py b/cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/handler.py new file mode 100644 index 0000000..6768278 --- /dev/null +++ b/cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/handler.py @@ -0,0 +1,268 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates +_sent_hashes: set = set() + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + import hashlib + h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12] + if h in _sent_hashes: + print(f'[agent-runner] dedup: skipping duplicate message (hash={h})') + return + _sent_hashes.add(h) + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + try: + resp = urllib.request.urlopen(req, timeout=10) + resp_body = resp.read() + import re + msg_id = re.search(r'"message_id":(\d+)', resp_body.decode('utf-8', errors='replace')) + print(f'[agent-runner] Telegram sendMessage -> msg_id={msg_id.group(1) if msg_id else "?"} hash={h}') + except Exception as e: + print(f'[agent-runner] Telegram sendMessage FAILED: {type(e).__name__}: {e} hash={h}') + raise + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta ONLY + # Do NOT use event.get('data') — that's the full formatted summary, + # causing duplicate delivery alongside the token stream. + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') + if token: + text_buffer += token + # Only flush if buffer is very large — prevents splitting multi-turn responses + if len(text_buffer) > 1200: + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/requirements.txt b/cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.0bef0f748c2ff4085fd7646e04311bd0bec377fa20fc8a0b4cf4be3998efe0b4/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/handler.py b/cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/handler.py new file mode 100644 index 0000000..ff5226e --- /dev/null +++ b/cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/handler.py @@ -0,0 +1,232 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + if body is not None: + for chunk in body.iter_chunks(): + if not chunk: + continue + try: + event = json.loads(chunk.decode('utf-8')) + # Strands streaming event: 'data' field contains text delta + delta = event.get('data', '') or event.get('text', '') + if delta: + text_buffer += delta + # Flush on paragraph or sentence break, or if buffer is large + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + except (json.JSONDecodeError, UnicodeDecodeError): + pass + + # Flush any remaining text + if text_buffer.strip() and bot_token: + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/requirements.txt b/cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.1a89c2236890bf44d0c026a8bb7aa12b9b8a02598259b8977593795f6e631143/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/handler.py b/cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/handler.py new file mode 100644 index 0000000..7aba474 --- /dev/null +++ b/cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/handler.py @@ -0,0 +1,263 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates +_sent_hashes: set = set() + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + import hashlib, traceback as tb + h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12] + if h in _sent_hashes: + print(f'[agent-runner] dedup: skipping duplicate message (hash={h})') + print(f'[agent-runner] dedup stack: {tb.format_stack()[-3].strip()}') + return + _sent_hashes.add(h) + print(f'[agent-runner] SEND hash={h} text={repr(text[:40])}') + print(f'[agent-runner] SEND caller: {tb.format_stack()[-2].strip()}') + url = f'https://api.telegram.org/bot{token}/sendMessage' + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') or event.get('data', '') + if token: + text_buffer += token + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/requirements.txt b/cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.49cb91f68c283b98bae3da4b66724b2d4daaeecdfdbeffb7b1ceb3d9f233d8aa/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/handler.py b/cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/handler.py new file mode 100644 index 0000000..a1ed14f --- /dev/null +++ b/cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/handler.py @@ -0,0 +1,268 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates +_sent_hashes: set = set() + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + import hashlib + h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12] + if h in _sent_hashes: + print(f'[agent-runner] dedup: skipping duplicate message (hash={h})') + return + _sent_hashes.add(h) + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + try: + resp = urllib.request.urlopen(req, timeout=10) + resp_body = resp.read() + import re + msg_id = re.search(r'"message_id":(\d+)', resp_body.decode('utf-8', errors='replace')) + print(f'[agent-runner] Telegram sendMessage -> msg_id={msg_id.group(1) if msg_id else "?"} hash={h}') + except Exception as e: + print(f'[agent-runner] Telegram sendMessage FAILED: {type(e).__name__}: {e} hash={h}') + raise + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('enrolled_services', user_profile.get('services', {})), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta ONLY + # Do NOT use event.get('data') — that's the full formatted summary, + # causing duplicate delivery alongside the token stream. + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') + if token: + text_buffer += token + # Only flush if buffer is very large — prevents splitting multi-turn responses + if len(text_buffer) > 1200: + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/requirements.txt b/cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/handler.py b/cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/handler.py new file mode 100644 index 0000000..390a95b --- /dev/null +++ b/cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/handler.py @@ -0,0 +1,260 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates +_sent_hashes: set = set() + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + import hashlib + h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12] + if h in _sent_hashes: + print(f'[agent-runner] dedup: skipping duplicate message (hash={h})') + return + _sent_hashes.add(h) + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta ONLY + # Do NOT use event.get('data') — that's the full formatted summary, + # causing duplicate delivery alongside the token stream. + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') + if token: + text_buffer += token + # Only flush if buffer is very large — prevents splitting multi-turn responses + if len(text_buffer) > 1200: + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/requirements.txt b/cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.72ae50dccfdc4ec6da7bb3d626e4f550bd570a6e092e76dc5d243e2aba10d018/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/handler.py b/cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/handler.py new file mode 100644 index 0000000..ab74dd4 --- /dev/null +++ b/cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/handler.py @@ -0,0 +1,244 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + # Extract text delta from contentBlockDelta + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + token = delta.get('text', '') or event.get('data', '') + if token: + text_buffer += token + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + if text_buffer.strip() and bot_token: + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/requirements.txt b/cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.7edb4c1c824b04027f9d286b81ec5c52b21a60fe8d11b578714edc68a5bd9113/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/handler.py b/cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/handler.py new file mode 100644 index 0000000..cd35982 --- /dev/null +++ b/cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/handler.py @@ -0,0 +1,251 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') or event.get('data', '') + if token: + text_buffer += token + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/requirements.txt b/cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.83d1ec88ed482efeb08304b8bd291b1d2bcba46ec44c03c1b411b7703af8c47d/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/handler.py b/cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/handler.py new file mode 100644 index 0000000..5773a7f --- /dev/null +++ b/cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/handler.py @@ -0,0 +1,246 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + token = delta.get('text', '') or event.get('data', '') + if token: + text_buffer += token + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + if text_buffer.strip() and bot_token: + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/requirements.txt b/cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.858dc60141c52de33597fc8d6855b7f1e2c7106e43f2d0ed2772f0a1c63460cc/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f/handler.py b/cdk/cdk.out/asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f/handler.py new file mode 100644 index 0000000..ced960f --- /dev/null +++ b/cdk/cdk.out/asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f/handler.py @@ -0,0 +1,29 @@ +"""EventBridge-triggered Lambda: sends a Telegram reminder then deletes the rule.""" +import json +import os +import boto3 +import urllib.request + + +def handler(event, context): + chat_id = event['chat_id'] + message = event['message'] + rule_name = event['rule_name'] + + # Fetch bot token + sm = boto3.client('secretsmanager', region_name='us-east-1') + token = sm.get_secret_value(SecretId=os.environ['TELEGRAM_BOT_TOKEN_SECRET_ARN'])['SecretString'] + + # Send Telegram message + payload = json.dumps({'chat_id': chat_id, 'text': message}).encode() + req = urllib.request.Request( + f'https://api.telegram.org/bot{token}/sendMessage', + data=payload, + headers={'Content-Type': 'application/json'}, + ) + urllib.request.urlopen(req) + + # Delete the one-time rule + eb = boto3.client('events', region_name='us-east-1') + eb.remove_targets(Rule=rule_name, Ids=['scheduler']) + eb.delete_rule(Name=rule_name) diff --git a/cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/handler.py b/cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/handler.py new file mode 100644 index 0000000..8eb39d8 --- /dev/null +++ b/cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/handler.py @@ -0,0 +1,196 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + _agentcore = boto3.client('bedrock-agentcore', region_name='us-east-1') + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Drain streaming response body (agent delivers to Telegram via send_message tool) + body = response.get('response') + if body is not None: + for _ in body.iter_chunks(): + pass + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/requirements.txt b/cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.b39a50854f31ec1201e5148be458f2a5fc66785d31998cfa942892d925fd807e/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/handler.py b/cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/handler.py new file mode 100644 index 0000000..a58c69e --- /dev/null +++ b/cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/handler.py @@ -0,0 +1,261 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates +_sent_hashes: set = set() + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + import hashlib + h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12] + if h in _sent_hashes: + print(f'[agent-runner] dedup: skipping duplicate message (hash={h})') + return + _sent_hashes.add(h) + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') or event.get('data', '') + if token: + text_buffer += token + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/requirements.txt b/cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.e2534215509b6a3945aa5d9f474d3b7b8e09e8116ee642b23ea53ceb1a33d528/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/handler.py b/cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/handler.py new file mode 100644 index 0000000..2d00b17 --- /dev/null +++ b/cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/handler.py @@ -0,0 +1,263 @@ +import json +import os +import time +import uuid +import boto3 +import urllib.request +from typing import Any + +# AWS clients +_ddb = None +_agentcore = None + + +def get_ddb(): + global _ddb + if _ddb is None: + _ddb = boto3.resource('dynamodb') + return _ddb + + +def get_agentcore(): + global _agentcore + if _agentcore is None: + from botocore.config import Config + _agentcore = boto3.client( + 'bedrock-agentcore', + region_name='us-east-1', + config=Config(read_timeout=600, connect_timeout=10) + ) + return _agentcore + + +def get_or_create_user(actor_id: str, from_info: dict) -> dict: + """Look up user in registry, auto-registering on first contact.""" + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return {'actor_id': actor_id, 'display_name': from_info.get('from_name', actor_id)} + table = get_ddb().Table(table_name) + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + if item: + return item + now = int(time.time()) + item = { + 'actor_id': actor_id, + 'display_name': from_info.get('from_name') or actor_id, + 'telegram_username': from_info.get('from_username', ''), + 'created_at': str(now), + 'status': 'pending', + 'services': {}, + } + table.put_item(Item=item) + print(f'[agent-runner] Registered new user (pending): {actor_id}') + return item + + +def update_user_status(actor_id: str, name: str, status: str) -> None: + table_name = os.environ.get('USERS_TABLE_NAME', '') + if not table_name: + return + table = get_ddb().Table(table_name) + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET display_name = :n, #s = :s', + ExpressionAttributeNames={'#s': 'status'}, + ExpressionAttributeValues={':n': name, ':s': status}, + ) + + +# Per-invocation dedup: track sent message hashes to prevent AgentCore retry duplicates +_sent_hashes: set = set() + + +def send_telegram_direct(chat_id: str, token: str, text: str) -> None: + import hashlib + h = hashlib.md5(f'{chat_id}:{text}'.encode()).hexdigest()[:12] + if h in _sent_hashes: + print(f'[agent-runner] dedup: skipping duplicate message (hash={h})') + return + _sent_hashes.add(h) + url = f'https://api.telegram.org/bot{token}/sendMessage' + data = json.dumps({'chat_id': chat_id, 'text': text}).encode() + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=10) + + +def get_or_create_session(actor_id: str) -> str: + """Look up active session for actor, or create a new one.""" + table = get_ddb().Table(os.environ['SESSION_TABLE_NAME']) + + response = table.get_item(Key={'actor_id': actor_id}) + item = response.get('Item') + + now = int(time.time()) + ttl_8hr = now + (8 * 3600) + + if item and item.get('ttl', 0) > now: + # Active session exists — extend TTL + table.update_item( + Key={'actor_id': actor_id}, + UpdateExpression='SET #ttl = :ttl', + ExpressionAttributeNames={'#ttl': 'ttl'}, + ExpressionAttributeValues={':ttl': ttl_8hr}, + ) + return item['session_id'] + + # Create new session + session_id = str(uuid.uuid4()) + table.put_item(Item={ + 'actor_id': actor_id, + 'session_id': session_id, + 'created_at': str(now), + 'ttl': ttl_8hr, + }) + return session_id + + +def handler(event, context): + # ── Parse SQS records (FIFO — all from same actor) ─────────────────── + records = [] + for record in event.get('Records', []): + try: + records.append(json.loads(record['body'])) + except (json.JSONDecodeError, KeyError): + continue + + if not records: + return + + first = records[0] + channel = first.get('channel', 'telegram') + chat_id = first.get('chat_id', '') + actor_id = f"{channel}:{chat_id}" + + # ── User registry ───────────────────────────────────────────────────── + from_info = first.get('messages', [{}])[0] + user_profile = get_or_create_user(actor_id, from_info) + + # ── Onboarding gate ───────────────────────────────────────────────────── + table_name = os.environ.get('USERS_TABLE_NAME', '') + if table_name and user_profile.get('status', 'active') == 'pending': + raw_prompt = records[0]['messages'][0]['text'] if records else '' + is_name_msg = bool(raw_prompt and len(raw_prompt.strip()) < 50 and '?' not in raw_prompt) + if is_name_msg: + name = raw_prompt.strip() + update_user_status(actor_id, name=name, status='active') + user_profile['display_name'] = name + user_profile['status'] = 'active' + prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" + else: + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + bot_token = '' + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?") + return + # ── Get or create AgentCore session ────────────────────────────────── + session_id = get_or_create_session(actor_id) + print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}") + + # ── Bundle messages ─────────────────────────────────────────────────── + if len(records) == 1: + prompt = records[0]['messages'][0]['text'] + else: + lines = [ + f"[{i+1}] {r['messages'][0]['text']}" + for i, r in enumerate(records) + ] + prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) + + # ── Build payload for AgentCore Runtime 1 ──────────────────────────── + payload: dict[str, Any] = { + 'prompt': prompt, + 'actor_id': actor_id, + 'session_id': session_id, + 'user_profile': { + 'display_name': user_profile.get('display_name', actor_id), + 'telegram_username': user_profile.get('telegram_username', ''), + 'google_email': user_profile.get('google_email', ''), + 'allowed': user_profile.get('allowed', True), + 'services': user_profile.get('services', {}), + }, + 'channel_adapter': { + 'type': channel, + 'target_id': str(chat_id), + 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), + }, + } + + # ── Invoke AgentCore Runtime 1 ──────────────────────────────────────── + runtime_arn = os.environ.get('RUNTIME_1_ARN', '') + if not runtime_arn or runtime_arn == 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY': + print(f"[agent-runner] RUNTIME_1_ARN not set — skipping AgentCore invoke") + print(f"[agent-runner] Would have sent: {json.dumps(payload)[:200]}") + return + + client = get_agentcore() + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload).encode(), + ) + + # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive + bot_token = '' + bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') + if bot_token_secret_arn: + sm = boto3.client('secretsmanager', region_name='us-east-1') + try: + bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] + except Exception as e: + print(f'[agent-runner] Failed to get bot token: {e}') + + body = response.get('response') + text_buffer = '' + leftover = '' + if body is not None: + for raw_chunk in body.iter_chunks(): + if not raw_chunk: + continue + # AgentCore streams SSE format: "data: {...}\n\n" + text = leftover + raw_chunk.decode('utf-8', errors='replace') + parts = text.split('\n\n') + leftover = parts[-1] + for part in parts[:-1]: + for line in part.splitlines(): + if not line.startswith('data: '): + continue + data = line[6:].strip() + if not data or data == '[DONE]': + continue + try: + event = json.loads(data) + except (json.JSONDecodeError, ValueError): + continue + if not isinstance(event, dict): + continue + # Extract text delta from contentBlockDelta ONLY + # Do NOT use event.get('data') — that's the full formatted summary, + # causing duplicate delivery alongside the token stream. + delta = event.get('event', {}).get('contentBlockDelta', {}).get('delta', {}) + if not isinstance(delta, dict): + continue + token = delta.get('text', '') + if token: + text_buffer += token + flush = ( + text_buffer.rstrip().endswith(('\n\n', '.\n', '!\n', '?\n')) + or len(text_buffer) > 800 + ) + if flush and text_buffer.strip(): + print(f'[agent-runner] send chunk {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + text_buffer = '' + + # Flush any remaining text + print(f'[agent-runner] stream done buffer={len(text_buffer)} bot_token_set={bool(bot_token)}') + if text_buffer.strip() and bot_token: + print(f'[agent-runner] flushing {len(text_buffer)}c to {chat_id}') + send_telegram_direct(str(chat_id), bot_token, text_buffer.strip()) + + print(f"[agent-runner] Completed session={session_id} actor={actor_id}") diff --git a/cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/requirements.txt b/cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/requirements.txt new file mode 100644 index 0000000..011ba23 --- /dev/null +++ b/cdk/cdk.out/asset.eebdc8b902a677f1a4ba9aa8134495f429b3ccffecd170e6fa5b8c46361af321/requirements.txt @@ -0,0 +1 @@ +boto3>=1.34.0 diff --git a/cdk/cdk.out/manifest.json b/cdk/cdk.out/manifest.json index a99a7ce..1bfa4ec 100644 --- a/cdk/cdk.out/manifest.json +++ b/cdk/cdk.out/manifest.json @@ -18,7 +18,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-deploy-role-495395224548-us-east-1", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-cfn-exec-role-495395224548-us-east-1", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/a31aaa0bc9eab4fd6f17f10795fba05983dba0c88e83a263fe9fffe930da06b9.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/c6cd323425a93776b45e2e0806064efbc5c84a3d6d78532282df6dd62cc14bda.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/cdk/cdk.out/tree.json b/cdk/cdk.out/tree.json index 625dffd..af4c4e0 100644 --- a/cdk/cdk.out/tree.json +++ b/cdk/cdk.out/tree.json @@ -1 +1 @@ -{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.252.0"},"children":{"AgentClawStack":{"id":"AgentClawStack","path":"AgentClawStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.252.0"},"children":{"TelegramBotToken":{"id":"TelegramBotToken","path":"AgentClawStack/TelegramBotToken","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"BraveApiKey":{"id":"BraveApiKey","path":"AgentClawStack/BraveApiKey","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceBucket":{"id":"WorkspaceBucket","path":"AgentClawStack/WorkspaceBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}},"SessionStore":{"id":"SessionStore","path":"AgentClawStack/SessionStore","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/SessionStore/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"SessionStore8C86EEFE","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-sessions","timeToLiveSpecification":{"attributeName":"ttl","enabled":true}}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/SessionStore/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"UsersTable":{"id":"UsersTable","path":"AgentClawStack/UsersTable","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/UsersTable/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"UsersTable9725E9C8","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-users"}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/UsersTable/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"MessageQueue":{"id":"MessageQueue","path":"AgentClawStack/MessageQueue","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.Queue","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/MessageQueue/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.CfnQueue","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SQS::Queue","aws:cdk:cloudformation:logicalId":"MessageQueue7A3BF959","aws:cdk:cloudformation:props":{"contentBasedDeduplication":false,"fifoQueue":true,"queueName":"agent-claw-messages.fifo","receiveMessageWaitTimeSeconds":20,"visibilityTimeout":900}}}}},"TgIngest":{"id":"TgIngest","path":"AgentClawStack/TgIngest","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/TgIngest/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleB96980B6","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleDefaultPolicyCC51E135","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["sqs:SendMessage","sqs:GetQueueAttributes","sqs:GetQueueUrl"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"}],"Version":"2012-10-17"},"policyName":"TgIngestServiceRoleDefaultPolicyCC51E135","roles":[{"Ref":"TgIngestServiceRoleB96980B6"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/TgIngest/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/TgIngest/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/TgIngest/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"TgIngest4CB35C2F","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875.zip"},"environment":{"variables":{"MESSAGE_QUEUE_URL":{"Ref":"MessageQueue7A3BF959"},"TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","TELEGRAM_WEBHOOK_SECRET":""}},"functionName":"agent-claw-tg-ingest","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["TgIngestServiceRoleB96980B6","Arn"]},"runtime":"python3.12","timeout":10}}}}},"AgentRunner":{"id":"AgentRunner","path":"AgentClawStack/AgentRunner","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/AgentRunner/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRole40CA0A00","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["sqs:ReceiveMessage","sqs:ChangeMessageVisibility","sqs:GetQueueUrl","sqs:DeleteMessage","sqs:GetQueueAttributes"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":"bedrock-agentcore:InvokeAgentRuntime","Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"},"policyName":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","roles":[{"Ref":"AgentRunnerServiceRole40CA0A00"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/AgentRunner/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/AgentRunner/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/AgentRunner/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"AgentRunnerBDE3FA56","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"c0db2a060be885d61722dfe6fbd3967e1c956826682078f6338123bf0c797e5b.zip"},"environment":{"variables":{"SESSION_TABLE_NAME":{"Ref":"SessionStore8C86EEFE"},"WORKSPACE_BUCKET_NAME":"agent-claw-workspace-495395224548","TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","BRAVE_API_KEY_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi","RUNTIME_1_ARN":"arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON","AWS_REGION_NAME":"us-east-1","USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"WORKSPACE_MCP_URL":"https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp"}},"functionName":"agent-claw-agent-runner","handler":"handler.handler","memorySize":256,"role":{"Fn::GetAtt":["AgentRunnerServiceRole40CA0A00","Arn"]},"runtime":"python3.12","timeout":900}}},"SqsEventSource:AgentClawStackMessageQueue9AF4DF23":{"id":"SqsEventSource:AgentClawStackMessageQueue9AF4DF23","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.EventSourceMapping","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnEventSourceMapping","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::EventSourceMapping","aws:cdk:cloudformation:logicalId":"AgentRunnerSqsEventSourceAgentClawStackMessageQueue9AF4DF234671B32B","aws:cdk:cloudformation:props":{"batchSize":10,"enabled":true,"eventSourceArn":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]},"functionName":{"Ref":"AgentRunnerBDE3FA56"}}}}}}}},"WebhookApi":{"id":"WebhookApi","path":"AgentClawStack/WebhookApi","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpApi","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnApi","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Api","aws:cdk:cloudformation:logicalId":"WebhookApi28122C53","aws:cdk:cloudformation:props":{"name":"agent-claw-webhook","protocolType":"HTTP"}}},"DefaultStage":{"id":"DefaultStage","path":"AgentClawStack/WebhookApi/DefaultStage","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpStage","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/DefaultStage/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnStage","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Stage","aws:cdk:cloudformation:logicalId":"WebhookApiDefaultStageC0BC9CA5","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"autoDeploy":true,"stageName":"$default"}}}}},"POST--telegram":{"id":"POST--telegram","path":"AgentClawStack/WebhookApi/POST--telegram","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"TgIngestIntegration":{"id":"TgIngestIntegration","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"payloadFormatVersion":"2.0"}}}}},"TgIngestIntegration-Permission":{"id":"TgIngestIntegration-Permission","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegrationPermissionFEBC2E3B","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/telegram"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramF7127CFF","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"POST /telegram","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85"}]]}}}}}},"GET--oauth--start":{"id":"GET--oauth--start","path":"AgentClawStack/WebhookApi/GET--oauth--start","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"OAuthStartIntegration":{"id":"OAuthStartIntegration","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstartOAuthStartIntegrationA546443F","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"payloadFormatVersion":"2.0"}}}}},"OAuthStartIntegration-Permission":{"id":"OAuthStartIntegration-Permission","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstartOAuthStartIntegrationPermission38BAEF6D","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/oauth/start"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--start/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstart6DCA713A","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"GET /oauth/start","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiGEToauthstartOAuthStartIntegrationA546443F"}]]}}}}}},"GET--oauth--callback":{"id":"GET--oauth--callback","path":"AgentClawStack/WebhookApi/GET--oauth--callback","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"OAuthCallbackIntegration":{"id":"OAuthCallbackIntegration","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"payloadFormatVersion":"2.0"}}}}},"OAuthCallbackIntegration-Permission":{"id":"OAuthCallbackIntegration-Permission","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationPermission6BA3A5AD","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/oauth/callback"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--callback/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackFC1F6BCD","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"GET /oauth/callback","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09"}]]}}}}}}}},"Runtime1Role":{"id":"Runtime1Role","path":"AgentClawStack/Runtime1Role","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"Runtime1RoleA7A82078","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"bedrock-agentcore.amazonaws.com"}}],"Version":"2012-10-17"},"description":"Execution role for agent-claw Runtime 1 (main assistant)"}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/Runtime1Role/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"Runtime1RoleDefaultPolicy1A3D5ACF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["bedrock:InvokeModel","bedrock:InvokeModelWithResponseStream"],"Effect":"Allow","Resource":"*"},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["bedrock-agentcore:CreateEvent","bedrock-agentcore:ListEvents","bedrock-agentcore:RetrieveMemoryRecords"],"Effect":"Allow","Resource":"*"},{"Action":"lambda:InvokeFunctionUrl","Condition":{"StringEquals":{"lambda:FunctionUrlAuthType":"AWS_IAM"}},"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":lambda:us-east-1:495395224548:function:agent-claw-workspace-mcp"]]},"Sid":"WorkspaceMcpInvoke"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":"secretsmanager:GetSecretValue","Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsReadRuntime"}],"Version":"2012-10-17"},"policyName":"Runtime1RoleDefaultPolicy1A3D5ACF","roles":[{"Ref":"Runtime1RoleA7A82078"}]}}}}}}},"GoogleOAuthClient":{"id":"GoogleOAuthClient","path":"AgentClawStack/GoogleOAuthClient","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceMcpRole":{"id":"WorkspaceMcpRole","path":"AgentClawStack/WorkspaceMcpRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"},"children":{"Policy":{"id":"Policy","path":"AgentClawStack/WorkspaceMcpRole/Policy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WorkspaceMcpRole/Policy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"WorkspaceMcpRolePolicy5B8B0072","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":"secretsmanager:GetSecretValue","Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsRead"}],"Version":"2012-10-17"},"policyName":"WorkspaceMcpRolePolicy5B8B0072","roles":["agent-claw-workspace-mcp-role"]}}}}}}},"WorkspaceMcp":{"id":"WorkspaceMcp","path":"AgentClawStack/WorkspaceMcp","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.FunctionBase","version":"2.252.0"}},"OAuthHandler":{"id":"OAuthHandler","path":"AgentClawStack/OAuthHandler","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/OAuthHandler/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"OAuthHandlerServiceRole9CDCCF9E","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"OAuthHandlerServiceRoleDefaultPolicy69D90416","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["secretsmanager:CreateSecret","secretsmanager:PutSecretValue","secretsmanager:GetSecretValue"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsWrite"}],"Version":"2012-10-17"},"policyName":"OAuthHandlerServiceRoleDefaultPolicy69D90416","roles":[{"Ref":"OAuthHandlerServiceRole9CDCCF9E"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/OAuthHandler/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/OAuthHandler/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/OAuthHandler/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"OAuthHandlerC97C2476","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip"},"environment":{"variables":{"GOOGLE_OAUTH_CLIENT_SECRET_ARN":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client"]]},"USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"OAUTH_REDIRECT_URI":{"Fn::Join":["",["https://",{"Ref":"WebhookApi28122C53"},".execute-api.us-east-1.",{"Ref":"AWS::URLSuffix"},"/oauth/callback"]]}}},"functionName":"agent-claw-oauth-handler","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["OAuthHandlerServiceRole9CDCCF9E","Arn"]},"runtime":"python3.12","timeout":30}}}}},"WorkspaceMcpFunctionUrl":{"id":"WorkspaceMcpFunctionUrl","path":"AgentClawStack/WorkspaceMcpFunctionUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"OAuthStartUrl":{"id":"OAuthStartUrl","path":"AgentClawStack/OAuthStartUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"OAuthRedirectUri":{"id":"OAuthRedirectUri","path":"AgentClawStack/OAuthRedirectUri","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WebhookUrl":{"id":"WebhookUrl","path":"AgentClawStack/WebhookUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WorkspaceBucketName":{"id":"WorkspaceBucketName","path":"AgentClawStack/WorkspaceBucketName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"SessionTableName":{"id":"SessionTableName","path":"AgentClawStack/SessionTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"UsersTableName":{"id":"UsersTableName","path":"AgentClawStack/UsersTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"MessageQueueUrl":{"id":"MessageQueueUrl","path":"AgentClawStack/MessageQueueUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"Runtime1RoleArn":{"id":"Runtime1RoleArn","path":"AgentClawStack/Runtime1RoleArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"CDKMetadata":{"id":"CDKMetadata","path":"AgentClawStack/CDKMetadata","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Default":{"id":"Default","path":"AgentClawStack/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.252.0"}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"AgentClawStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.252.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"AgentClawStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.252.0"}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}}}}} \ No newline at end of file +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"2.252.0"},"children":{"AgentClawStack":{"id":"AgentClawStack","path":"AgentClawStack","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"2.252.0"},"children":{"TelegramBotToken":{"id":"TelegramBotToken","path":"AgentClawStack/TelegramBotToken","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"BraveApiKey":{"id":"BraveApiKey","path":"AgentClawStack/BraveApiKey","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceBucket":{"id":"WorkspaceBucket","path":"AgentClawStack/WorkspaceBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}},"SessionStore":{"id":"SessionStore","path":"AgentClawStack/SessionStore","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/SessionStore/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"SessionStore8C86EEFE","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-sessions","timeToLiveSpecification":{"attributeName":"ttl","enabled":true}}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/SessionStore/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"UsersTable":{"id":"UsersTable","path":"AgentClawStack/UsersTable","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.Table","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/UsersTable/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_dynamodb.CfnTable","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::DynamoDB::Table","aws:cdk:cloudformation:logicalId":"UsersTable9725E9C8","aws:cdk:cloudformation:props":{"attributeDefinitions":[{"attributeName":"actor_id","attributeType":"S"}],"billingMode":"PAY_PER_REQUEST","keySchema":[{"attributeName":"actor_id","keyType":"HASH"}],"tableName":"agent-claw-users"}}},"ScalingRole":{"id":"ScalingRole","path":"AgentClawStack/UsersTable/ScalingRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}}}},"MessageQueue":{"id":"MessageQueue","path":"AgentClawStack/MessageQueue","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.Queue","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/MessageQueue/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_sqs.CfnQueue","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::SQS::Queue","aws:cdk:cloudformation:logicalId":"MessageQueue7A3BF959","aws:cdk:cloudformation:props":{"contentBasedDeduplication":false,"fifoQueue":true,"queueName":"agent-claw-messages.fifo","receiveMessageWaitTimeSeconds":20,"visibilityTimeout":900}}}}},"TgIngest":{"id":"TgIngest","path":"AgentClawStack/TgIngest","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/TgIngest/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleB96980B6","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"TgIngestServiceRoleDefaultPolicyCC51E135","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["sqs:SendMessage","sqs:GetQueueAttributes","sqs:GetQueueUrl"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"}],"Version":"2012-10-17"},"policyName":"TgIngestServiceRoleDefaultPolicyCC51E135","roles":[{"Ref":"TgIngestServiceRoleB96980B6"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/TgIngest/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/TgIngest/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/TgIngest/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/TgIngest/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"TgIngest4CB35C2F","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875.zip"},"environment":{"variables":{"MESSAGE_QUEUE_URL":{"Ref":"MessageQueue7A3BF959"},"TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","TELEGRAM_WEBHOOK_SECRET":""}},"functionName":"agent-claw-tg-ingest","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["TgIngestServiceRoleB96980B6","Arn"]},"runtime":"python3.12","timeout":10}}}}},"AgentRunner":{"id":"AgentRunner","path":"AgentClawStack/AgentRunner","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/AgentRunner/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRole40CA0A00","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["SessionStore8C86EEFE","Arn"]}]},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["sqs:ReceiveMessage","sqs:ChangeMessageVisibility","sqs:GetQueueUrl","sqs:DeleteMessage","sqs:GetQueueAttributes"],"Effect":"Allow","Resource":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]}},{"Action":"bedrock-agentcore:InvokeAgentRuntime","Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"},"policyName":"AgentRunnerServiceRoleDefaultPolicyA584A5CF","roles":[{"Ref":"AgentRunnerServiceRole40CA0A00"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/AgentRunner/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/AgentRunner/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/AgentRunner/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"AgentRunnerBDE3FA56","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"49f9e3ee598c0259165125872304200dbdffee263d76fca541a8630534d8f5c5.zip"},"environment":{"variables":{"SESSION_TABLE_NAME":{"Ref":"SessionStore8C86EEFE"},"WORKSPACE_BUCKET_NAME":"agent-claw-workspace-495395224548","TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3","BRAVE_API_KEY_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi","RUNTIME_1_ARN":"arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON","AWS_REGION_NAME":"us-east-1","USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"WORKSPACE_MCP_URL":"https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp"}},"functionName":"agent-claw-agent-runner","handler":"handler.handler","memorySize":256,"role":{"Fn::GetAtt":["AgentRunnerServiceRole40CA0A00","Arn"]},"runtime":"python3.12","timeout":900}}},"SqsEventSource:AgentClawStackMessageQueue9AF4DF23":{"id":"SqsEventSource:AgentClawStackMessageQueue9AF4DF23","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.EventSourceMapping","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/AgentRunner/SqsEventSource:AgentClawStackMessageQueue9AF4DF23/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnEventSourceMapping","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::EventSourceMapping","aws:cdk:cloudformation:logicalId":"AgentRunnerSqsEventSourceAgentClawStackMessageQueue9AF4DF234671B32B","aws:cdk:cloudformation:props":{"batchSize":10,"enabled":true,"eventSourceArn":{"Fn::GetAtt":["MessageQueue7A3BF959","Arn"]},"functionName":{"Ref":"AgentRunnerBDE3FA56"}}}}}}}},"WebhookApi":{"id":"WebhookApi","path":"AgentClawStack/WebhookApi","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpApi","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnApi","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Api","aws:cdk:cloudformation:logicalId":"WebhookApi28122C53","aws:cdk:cloudformation:props":{"name":"agent-claw-webhook","protocolType":"HTTP"}}},"DefaultStage":{"id":"DefaultStage","path":"AgentClawStack/WebhookApi/DefaultStage","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpStage","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/DefaultStage/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnStage","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Stage","aws:cdk:cloudformation:logicalId":"WebhookApiDefaultStageC0BC9CA5","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"autoDeploy":true,"stageName":"$default"}}}}},"POST--telegram":{"id":"POST--telegram","path":"AgentClawStack/WebhookApi/POST--telegram","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"TgIngestIntegration":{"id":"TgIngestIntegration","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"payloadFormatVersion":"2.0"}}}}},"TgIngestIntegration-Permission":{"id":"TgIngestIntegration-Permission","path":"AgentClawStack/WebhookApi/POST--telegram/TgIngestIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramTgIngestIntegrationPermissionFEBC2E3B","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["TgIngest4CB35C2F","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/telegram"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/POST--telegram/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiPOSTtelegramF7127CFF","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"POST /telegram","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiPOSTtelegramTgIngestIntegration9EE5BB85"}]]}}}}}},"GET--oauth--start":{"id":"GET--oauth--start","path":"AgentClawStack/WebhookApi/GET--oauth--start","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"OAuthStartIntegration":{"id":"OAuthStartIntegration","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstartOAuthStartIntegrationA546443F","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"payloadFormatVersion":"2.0"}}}}},"OAuthStartIntegration-Permission":{"id":"OAuthStartIntegration-Permission","path":"AgentClawStack/WebhookApi/GET--oauth--start/OAuthStartIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstartOAuthStartIntegrationPermission38BAEF6D","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/oauth/start"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--start/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthstart6DCA713A","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"GET /oauth/start","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiGEToauthstartOAuthStartIntegrationA546443F"}]]}}}}}},"GET--oauth--callback":{"id":"GET--oauth--callback","path":"AgentClawStack/WebhookApi/GET--oauth--callback","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpRoute","version":"2.252.0"},"children":{"OAuthCallbackIntegration":{"id":"OAuthCallbackIntegration","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpIntegration","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnIntegration","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Integration","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"integrationType":"AWS_PROXY","integrationUri":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"payloadFormatVersion":"2.0"}}}}},"OAuthCallbackIntegration-Permission":{"id":"OAuthCallbackIntegration-Permission","path":"AgentClawStack/WebhookApi/GET--oauth--callback/OAuthCallbackIntegration-Permission","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationPermission6BA3A5AD","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["OAuthHandlerC97C2476","Arn"]},"principal":"apigateway.amazonaws.com","sourceArn":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":execute-api:us-east-1:495395224548:",{"Ref":"WebhookApi28122C53"},"/*/*/oauth/callback"]]}}}},"Resource":{"id":"Resource","path":"AgentClawStack/WebhookApi/GET--oauth--callback/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnRoute","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Route","aws:cdk:cloudformation:logicalId":"WebhookApiGEToauthcallbackFC1F6BCD","aws:cdk:cloudformation:props":{"apiId":{"Ref":"WebhookApi28122C53"},"authorizationType":"NONE","routeKey":"GET /oauth/callback","target":{"Fn::Join":["",["integrations/",{"Ref":"WebhookApiGEToauthcallbackOAuthCallbackIntegrationCFBBEB09"}]]}}}}}}}},"Runtime1Role":{"id":"Runtime1Role","path":"AgentClawStack/Runtime1Role","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"Runtime1RoleA7A82078","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"bedrock-agentcore.amazonaws.com"}}],"Version":"2012-10-17"},"description":"Execution role for agent-claw Runtime 1 (main assistant)"}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/Runtime1Role/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Runtime1Role/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"Runtime1RoleDefaultPolicy1A3D5ACF","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["bedrock:InvokeModel","bedrock:InvokeModelWithResponseStream"],"Effect":"Allow","Resource":"*"},{"Action":["s3:GetObject*","s3:GetBucket*","s3:List*"],"Effect":"Allow","Resource":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548"]]},{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":s3:::agent-claw-workspace-495395224548/*"]]}]},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["bedrock-agentcore:CreateEvent","bedrock-agentcore:ListEvents","bedrock-agentcore:RetrieveMemoryRecords"],"Effect":"Allow","Resource":"*"},{"Action":"lambda:InvokeFunctionUrl","Condition":{"StringEquals":{"lambda:FunctionUrlAuthType":"AWS_IAM"}},"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":lambda:us-east-1:495395224548:function:agent-claw-workspace-mcp"]]},"Sid":"WorkspaceMcpInvoke"},{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":"secretsmanager:GetSecretValue","Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsReadRuntime"},{"Action":["events:PutRule","events:PutTargets","events:ListRules","events:ListTargetsByRule","events:RemoveTargets","events:DeleteRule"],"Effect":"Allow","Resource":"arn:aws:events:us-east-1:*:rule/agent-claw-reminder-*","Sid":"EventBridgeScheduler"},{"Action":["lambda:AddPermission","lambda:RemovePermission"],"Effect":"Allow","Resource":{"Fn::GetAtt":["SchedulerCFE73206","Arn"]},"Sid":"SchedulerLambdaPermission"}],"Version":"2012-10-17"},"policyName":"Runtime1RoleDefaultPolicy1A3D5ACF","roles":[{"Ref":"Runtime1RoleA7A82078"}]}}}}}}},"GoogleOAuthClient":{"id":"GoogleOAuthClient","path":"AgentClawStack/GoogleOAuthClient","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"}},"WorkspaceMcpRole":{"id":"WorkspaceMcpRole","path":"AgentClawStack/WorkspaceMcpRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"2.252.0"},"children":{"Policy":{"id":"Policy","path":"AgentClawStack/WorkspaceMcpRole/Policy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/WorkspaceMcpRole/Policy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"WorkspaceMcpRolePolicy5B8B0072","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":"secretsmanager:GetSecretValue","Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsRead"}],"Version":"2012-10-17"},"policyName":"WorkspaceMcpRolePolicy5B8B0072","roles":["agent-claw-workspace-mcp-role"]}}}}}}},"WorkspaceMcp":{"id":"WorkspaceMcp","path":"AgentClawStack/WorkspaceMcp","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.FunctionBase","version":"2.252.0"}},"OAuthHandler":{"id":"OAuthHandler","path":"AgentClawStack/OAuthHandler","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/OAuthHandler/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"OAuthHandlerServiceRole9CDCCF9E","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"OAuthHandlerServiceRoleDefaultPolicy69D90416","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"]]}},{"Action":["dynamodb:BatchGetItem","dynamodb:Query","dynamodb:GetItem","dynamodb:Scan","dynamodb:ConditionCheckItem","dynamodb:BatchWriteItem","dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:DeleteItem","dynamodb:DescribeTable"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["dynamodb:GetRecords","dynamodb:GetShardIterator"],"Effect":"Allow","Resource":[{"Fn::GetAtt":["UsersTable9725E9C8","Arn"]}]},{"Action":["secretsmanager:CreateSecret","secretsmanager:PutSecretValue","secretsmanager:GetSecretValue"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-credentials/*","Sid":"PerUserGoogleCredentialsWrite"}],"Version":"2012-10-17"},"policyName":"OAuthHandlerServiceRoleDefaultPolicy69D90416","roles":[{"Ref":"OAuthHandlerServiceRole9CDCCF9E"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/OAuthHandler/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/OAuthHandler/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/OAuthHandler/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/OAuthHandler/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"OAuthHandlerC97C2476","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"5be87975e51a6859dfad098b3d998a0bcd09a4f9a437bbf38923338fb559eb9e.zip"},"environment":{"variables":{"GOOGLE_OAUTH_CLIENT_SECRET_ARN":{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client"]]},"USERS_TABLE_NAME":{"Ref":"UsersTable9725E9C8"},"OAUTH_REDIRECT_URI":{"Fn::Join":["",["https://",{"Ref":"WebhookApi28122C53"},".execute-api.us-east-1.",{"Ref":"AWS::URLSuffix"},"/oauth/callback"]]}}},"functionName":"agent-claw-oauth-handler","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["OAuthHandlerServiceRole9CDCCF9E","Arn"]},"runtime":"python3.12","timeout":30}}}}},"Scheduler":{"id":"Scheduler","path":"AgentClawStack/Scheduler","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"2.252.0"},"children":{"ServiceRole":{"id":"ServiceRole","path":"AgentClawStack/Scheduler/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Scheduler/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:logicalId":"SchedulerServiceRole62CDA70C","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"AgentClawStack/Scheduler/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"2.252.0"},"children":{"Resource":{"id":"Resource","path":"AgentClawStack/Scheduler/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:logicalId":"SchedulerServiceRoleDefaultPolicyFA0D8235","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Effect":"Allow","Resource":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"},{"Action":["events:RemoveTargets","events:DeleteRule"],"Effect":"Allow","Resource":"arn:aws:events:us-east-1:495395224548:rule/agent-claw-reminder-*"}],"Version":"2012-10-17"},"policyName":"SchedulerServiceRoleDefaultPolicyFA0D8235","roles":[{"Ref":"SchedulerServiceRole62CDA70C"}]}}}}}}},"Code":{"id":"Code","path":"AgentClawStack/Scheduler/Code","constructInfo":{"fqn":"aws-cdk-lib.aws_s3_assets.Asset","version":"2.252.0"},"children":{"Stage":{"id":"Stage","path":"AgentClawStack/Scheduler/Code/Stage","constructInfo":{"fqn":"aws-cdk-lib.AssetStaging","version":"2.252.0"}},"AssetBucket":{"id":"AssetBucket","path":"AgentClawStack/Scheduler/Code/AssetBucket","constructInfo":{"fqn":"aws-cdk-lib.aws_s3.BucketBase","version":"2.252.0"}}}},"Resource":{"id":"Resource","path":"AgentClawStack/Scheduler/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:logicalId":"SchedulerCFE73206","aws:cdk:cloudformation:props":{"code":{"s3Bucket":"cdk-hnb659fds-assets-495395224548-us-east-1","s3Key":"8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip"},"environment":{"variables":{"TELEGRAM_BOT_TOKEN_SECRET_ARN":"arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"}},"functionName":"agent-claw-scheduler","handler":"handler.handler","memorySize":128,"role":{"Fn::GetAtt":["SchedulerServiceRole62CDA70C","Arn"]},"runtime":"python3.12","timeout":30}}},"EventBridgeInvoke":{"id":"EventBridgeInvoke","path":"AgentClawStack/Scheduler/EventBridgeInvoke","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnPermission","version":"2.252.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Permission","aws:cdk:cloudformation:logicalId":"SchedulerEventBridgeInvoke72A0529A","aws:cdk:cloudformation:props":{"action":"lambda:InvokeFunction","functionName":{"Fn::GetAtt":["SchedulerCFE73206","Arn"]},"principal":"events.amazonaws.com","sourceArn":"arn:aws:events:us-east-1:495395224548:rule/agent-claw-reminder-*"}}}}},"WorkspaceMcpFunctionUrl":{"id":"WorkspaceMcpFunctionUrl","path":"AgentClawStack/WorkspaceMcpFunctionUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"OAuthStartUrl":{"id":"OAuthStartUrl","path":"AgentClawStack/OAuthStartUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"OAuthRedirectUri":{"id":"OAuthRedirectUri","path":"AgentClawStack/OAuthRedirectUri","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WebhookUrl":{"id":"WebhookUrl","path":"AgentClawStack/WebhookUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"WorkspaceBucketName":{"id":"WorkspaceBucketName","path":"AgentClawStack/WorkspaceBucketName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"SessionTableName":{"id":"SessionTableName","path":"AgentClawStack/SessionTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"UsersTableName":{"id":"UsersTableName","path":"AgentClawStack/UsersTableName","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"MessageQueueUrl":{"id":"MessageQueueUrl","path":"AgentClawStack/MessageQueueUrl","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"Runtime1RoleArn":{"id":"Runtime1RoleArn","path":"AgentClawStack/Runtime1RoleArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"SchedulerLambdaArn":{"id":"SchedulerLambdaArn","path":"AgentClawStack/SchedulerLambdaArn","constructInfo":{"fqn":"aws-cdk-lib.CfnOutput","version":"2.252.0"}},"CDKMetadata":{"id":"CDKMetadata","path":"AgentClawStack/CDKMetadata","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"},"children":{"Default":{"id":"Default","path":"AgentClawStack/CDKMetadata/Default","constructInfo":{"fqn":"aws-cdk-lib.CfnResource","version":"2.252.0"}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"AgentClawStack/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"2.252.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"AgentClawStack/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"2.252.0"}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.6.0"}}}}} \ No newline at end of file diff --git a/cdk/lib/agent-claw-stack.ts b/cdk/lib/agent-claw-stack.ts index 291b353..1ffdc18 100644 --- a/cdk/lib/agent-claw-stack.ts +++ b/cdk/lib/agent-claw-stack.ts @@ -285,6 +285,47 @@ export class AgentClawStack extends cdk.Stack { // NOTE: AgentCore runtime env vars are set in agentcore.json / agentcore deploy, not CDK. // Output the URL so it can be manually set in agentcore.json OAUTH_START_URL. + + // ── Lambda: scheduler ───────────────────────────────────────────────── + const schedulerFn = new lambda.Function(this, 'Scheduler', { + functionName: 'agent-claw-scheduler', + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'handler.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../../src/lambdas/scheduler')), + timeout: cdk.Duration.seconds(30), + memorySize: 128, + environment: { + TELEGRAM_BOT_TOKEN_SECRET_ARN: telegramBotTokenSecretArn, + }, + }); + botTokenSecret.grantRead(schedulerFn); + // Allow EventBridge to invoke the scheduler Lambda + schedulerFn.addPermission('EventBridgeInvoke', { + principal: new iam.ServicePrincipal('events.amazonaws.com'), + sourceArn: `arn:aws:events:${this.region}:${this.account}:rule/agent-claw-reminder-*`, + }); + // Allow scheduler Lambda to delete its own EventBridge rule + schedulerFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['events:RemoveTargets', 'events:DeleteRule'], + resources: [`arn:aws:events:${this.region}:${this.account}:rule/agent-claw-reminder-*`], + })); + + // AgentCore runtime: EventBridge + Lambda permissions for scheduling + runtime1Role.addToPolicy(new iam.PolicyStatement({ + sid: 'EventBridgeScheduler', + actions: [ + 'events:PutRule', 'events:PutTargets', + 'events:ListRules', 'events:ListTargetsByRule', + 'events:RemoveTargets', 'events:DeleteRule', + ], + resources: [`arn:aws:events:us-east-1:*:rule/agent-claw-reminder-*`], + })); + runtime1Role.addToPolicy(new iam.PolicyStatement({ + sid: 'SchedulerLambdaPermission', + actions: ['lambda:AddPermission', 'lambda:RemovePermission'], + resources: [schedulerFn.functionArn], + })); + // ── Outputs ──────────────────────────────────────────────────────────── new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', { @@ -329,5 +370,10 @@ export class AgentClawStack extends cdk.Stack { value: runtime1Role.roleArn, description: 'IAM execution role ARN for AgentCore Runtime 1', }); + + new cdk.CfnOutput(this, 'SchedulerLambdaArn', { + value: schedulerFn.functionArn, + description: 'Scheduler Lambda ARN — set as SCHEDULER_LAMBDA_ARN in agentcore.json', + }); } } diff --git a/src/lambdas/scheduler/handler.py b/src/lambdas/scheduler/handler.py new file mode 100644 index 0000000..ced960f --- /dev/null +++ b/src/lambdas/scheduler/handler.py @@ -0,0 +1,29 @@ +"""EventBridge-triggered Lambda: sends a Telegram reminder then deletes the rule.""" +import json +import os +import boto3 +import urllib.request + + +def handler(event, context): + chat_id = event['chat_id'] + message = event['message'] + rule_name = event['rule_name'] + + # Fetch bot token + sm = boto3.client('secretsmanager', region_name='us-east-1') + token = sm.get_secret_value(SecretId=os.environ['TELEGRAM_BOT_TOKEN_SECRET_ARN'])['SecretString'] + + # Send Telegram message + payload = json.dumps({'chat_id': chat_id, 'text': message}).encode() + req = urllib.request.Request( + f'https://api.telegram.org/bot{token}/sendMessage', + data=payload, + headers={'Content-Type': 'application/json'}, + ) + urllib.request.urlopen(req) + + # Delete the one-time rule + eb = boto3.client('events', region_name='us-east-1') + eb.remove_targets(Rule=rule_name, Ids=['scheduler']) + eb.delete_rule(Name=rule_name)