From 841e729b18a0b897a5f9d0f8d72ca619242d8044 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 6 May 2026 21:11:07 -0500 Subject: [PATCH] Phase 1 cleanup: onboarding flow, per-user S3 MEMORY.md, seed script --- agentclaw/app/agent_claw_main/main.py | 2 +- .../app/agent_claw_main/prompt_builder.py | 59 +++++++++++++------ scripts/seed-users.py | 23 ++++++++ src/lambdas/agent-runner/handler.py | 44 +++++++++++++- 4 files changed, 107 insertions(+), 21 deletions(-) create mode 100755 scripts/seed-users.py diff --git a/agentclaw/app/agent_claw_main/main.py b/agentclaw/app/agent_claw_main/main.py index 04ab812..3b4f679 100644 --- a/agentclaw/app/agent_claw_main/main.py +++ b/agentclaw/app/agent_claw_main/main.py @@ -149,7 +149,7 @@ def main(payload: dict, context) -> dict: user_context = f'Name: {name}' if username: user_context += f'\nTelegram username: @{username}' - system_prompt = build_system_prompt(user_context=user_context) + system_prompt = build_system_prompt(user_context=user_context, actor_id=actor_id) # Model: claude-sonnet-4-6 via cross-region inference model = BedrockModel( diff --git a/agentclaw/app/agent_claw_main/prompt_builder.py b/agentclaw/app/agent_claw_main/prompt_builder.py index 6cbfab4..817e29a 100644 --- a/agentclaw/app/agent_claw_main/prompt_builder.py +++ b/agentclaw/app/agent_claw_main/prompt_builder.py @@ -1,34 +1,54 @@ import os import boto3 +from botocore.exceptions import ClientError -# Cache: built once per warm session (shared base only) -_base_prompt: str | None = None +# Cache keyed by actor_id ('' = global/no user) +_prompt_cache: dict[str, str] = {} -def build_system_prompt(user_context: str = '') -> str: +def build_system_prompt(user_context: str = '', actor_id: str = '') -> str: """Build system prompt from S3 workspace files + optional per-user context.""" - base = _get_base_prompt() + base = _get_base_prompt(actor_id) if user_context: return base + f'\n\n---\n\n## User\n{user_context}' return base -def _get_base_prompt() -> str: - global _base_prompt - if _base_prompt is not None: - return _base_prompt +def _get_base_prompt(actor_id: str = '') -> str: + if actor_id in _prompt_cache: + return _prompt_cache[actor_id] bucket = os.environ.get('WORKSPACE_BUCKET_NAME', '') or 'agent-claw-workspace-495395224548' - print(f'[prompt_builder] Loading from bucket: {bucket!r}') + print(f'[prompt_builder] Loading from bucket: {bucket!r} actor_id={actor_id!r}') if not bucket: print('[prompt_builder] WARNING: WORKSPACE_BUCKET_NAME not set!') - _base_prompt = 'You are a helpful personal assistant.' - return _base_prompt + _prompt_cache[actor_id] = 'You are a helpful personal assistant.' + return _prompt_cache[actor_id] s3 = boto3.client('s3') parts = [] + # Per-user MEMORY.md (falls back to global) + memory_key = f'users/{actor_id}/MEMORY.md' if actor_id else 'MEMORY.md' + try: + obj = s3.get_object(Bucket=bucket, Key=memory_key) + content = obj['Body'].read().decode('utf-8') + parts.append(f'## MEMORY.md\n{content}') + print(f'[prompt_builder] Loaded {memory_key} ({len(content)} bytes)') + except ClientError as e: + if e.response['Error']['Code'] in ('NoSuchKey', 'AccessDenied') and actor_id: + # Fall back to global MEMORY.md + try: + obj = s3.get_object(Bucket=bucket, Key='MEMORY.md') + content = obj['Body'].read().decode('utf-8') + parts.append(f'## MEMORY.md\n{content}') + print(f'[prompt_builder] Loaded MEMORY.md (fallback, {len(content)} bytes)') + except Exception as e2: + print(f'[prompt_builder] Failed to load MEMORY.md: {e2}') + else: + print(f'[prompt_builder] Failed to load {memory_key}: {e}') + for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'TOOLS.md']: try: obj = s3.get_object(Bucket=bucket, Key=fname) @@ -40,12 +60,15 @@ def _get_base_prompt() -> str: parts.append('## Runtime\nRuntime: agent-claw | host=AgentCore | model=bedrock-claude-sonnet | channel=telegram\nCurrent date/time is provided by the system. Timezone: America/Chicago.') - _base_prompt = '\n\n---\n\n'.join(parts) - print(f'[prompt_builder] Base prompt built: {len(_base_prompt)} chars') - return _base_prompt + result = '\n\n---\n\n'.join(parts) + _prompt_cache[actor_id] = result + print(f'[prompt_builder] Prompt built for actor_id={actor_id!r}: {len(result)} chars') + return result -def invalidate_prompt() -> None: - """Force rebuild of system prompt on next invocation (call after workspace write).""" - global _base_prompt - _base_prompt = None +def invalidate_prompt(actor_id: str = '') -> None: + """Invalidate cached prompt for a specific actor_id, or all if not specified.""" + if actor_id: + _prompt_cache.pop(actor_id, None) + else: + _prompt_cache.clear() diff --git a/scripts/seed-users.py b/scripts/seed-users.py new file mode 100755 index 0000000..366b62b --- /dev/null +++ b/scripts/seed-users.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# Run: AWS_PROFILE=ai1 python3 scripts/seed-users.py +import boto3 +from datetime import datetime, timezone + +session = boto3.Session(profile_name='ai1') +table = session.resource('dynamodb', region_name='us-east-1').Table('agent-claw-users') + +users = [ + { + 'actor_id': 'telegram:8537376738', + 'display_name': 'Daniel', + 'telegram_username': 'nessie_tn', + 'email': 'daniel@everyonce.com', + 'timezone': 'America/Chicago', + 'status': 'active', + 'enrolled_services': {}, + 'created_at': datetime.now(timezone.utc).isoformat(), + } +] +for u in users: + table.put_item(Item=u) + print(f"Seeded: {u['actor_id']} ({u['display_name']})") diff --git a/src/lambdas/agent-runner/handler.py b/src/lambdas/agent-runner/handler.py index 56680d5..051c651 100644 --- a/src/lambdas/agent-runner/handler.py +++ b/src/lambdas/agent-runner/handler.py @@ -3,6 +3,7 @@ import os import time import uuid import boto3 +import urllib.request from typing import Any # AWS clients @@ -40,13 +41,33 @@ def get_or_create_user(actor_id: str, from_info: dict) -> dict: 'display_name': from_info.get('from_name') or actor_id, 'telegram_username': from_info.get('from_username', ''), 'created_at': str(now), - 'allowed': True, + 'status': 'pending', } table.put_item(Item=item) - print(f'[agent-runner] Registered new user: {actor_id}') + 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']) @@ -99,6 +120,25 @@ def handler(event, context): 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', '')}")