Phase 1 cleanup: onboarding flow, per-user S3 MEMORY.md, seed script
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
23
scripts/seed-users.py
Executable file
23
scripts/seed-users.py
Executable file
@@ -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']})")
|
||||
@@ -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', '')}")
|
||||
|
||||
Reference in New Issue
Block a user