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}'
|
user_context = f'Name: {name}'
|
||||||
if username:
|
if username:
|
||||||
user_context += f'\nTelegram username: @{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: claude-sonnet-4-6 via cross-region inference
|
||||||
model = BedrockModel(
|
model = BedrockModel(
|
||||||
|
|||||||
@@ -1,34 +1,54 @@
|
|||||||
import os
|
import os
|
||||||
import boto3
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
# Cache: built once per warm session (shared base only)
|
# Cache keyed by actor_id ('' = global/no user)
|
||||||
_base_prompt: str | None = None
|
_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."""
|
"""Build system prompt from S3 workspace files + optional per-user context."""
|
||||||
base = _get_base_prompt()
|
base = _get_base_prompt(actor_id)
|
||||||
if user_context:
|
if user_context:
|
||||||
return base + f'\n\n---\n\n## User\n{user_context}'
|
return base + f'\n\n---\n\n## User\n{user_context}'
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
def _get_base_prompt() -> str:
|
def _get_base_prompt(actor_id: str = '') -> str:
|
||||||
global _base_prompt
|
if actor_id in _prompt_cache:
|
||||||
if _base_prompt is not None:
|
return _prompt_cache[actor_id]
|
||||||
return _base_prompt
|
|
||||||
|
|
||||||
bucket = os.environ.get('WORKSPACE_BUCKET_NAME', '') or 'agent-claw-workspace-495395224548'
|
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:
|
if not bucket:
|
||||||
print('[prompt_builder] WARNING: WORKSPACE_BUCKET_NAME not set!')
|
print('[prompt_builder] WARNING: WORKSPACE_BUCKET_NAME not set!')
|
||||||
_base_prompt = 'You are a helpful personal assistant.'
|
_prompt_cache[actor_id] = 'You are a helpful personal assistant.'
|
||||||
return _base_prompt
|
return _prompt_cache[actor_id]
|
||||||
|
|
||||||
s3 = boto3.client('s3')
|
s3 = boto3.client('s3')
|
||||||
parts = []
|
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']:
|
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'TOOLS.md']:
|
||||||
try:
|
try:
|
||||||
obj = s3.get_object(Bucket=bucket, Key=fname)
|
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.')
|
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)
|
result = '\n\n---\n\n'.join(parts)
|
||||||
print(f'[prompt_builder] Base prompt built: {len(_base_prompt)} chars')
|
_prompt_cache[actor_id] = result
|
||||||
return _base_prompt
|
print(f'[prompt_builder] Prompt built for actor_id={actor_id!r}: {len(result)} chars')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def invalidate_prompt() -> None:
|
def invalidate_prompt(actor_id: str = '') -> None:
|
||||||
"""Force rebuild of system prompt on next invocation (call after workspace write)."""
|
"""Invalidate cached prompt for a specific actor_id, or all if not specified."""
|
||||||
global _base_prompt
|
if actor_id:
|
||||||
_base_prompt = None
|
_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 time
|
||||||
import uuid
|
import uuid
|
||||||
import boto3
|
import boto3
|
||||||
|
import urllib.request
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
# AWS clients
|
# 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,
|
'display_name': from_info.get('from_name') or actor_id,
|
||||||
'telegram_username': from_info.get('from_username', ''),
|
'telegram_username': from_info.get('from_username', ''),
|
||||||
'created_at': str(now),
|
'created_at': str(now),
|
||||||
'allowed': True,
|
'status': 'pending',
|
||||||
}
|
}
|
||||||
table.put_item(Item=item)
|
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
|
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:
|
def get_or_create_session(actor_id: str) -> str:
|
||||||
"""Look up active session for actor, or create a new one."""
|
"""Look up active session for actor, or create a new one."""
|
||||||
table = get_ddb().Table(os.environ['SESSION_TABLE_NAME'])
|
table = get_ddb().Table(os.environ['SESSION_TABLE_NAME'])
|
||||||
@@ -99,6 +120,25 @@ def handler(event, context):
|
|||||||
from_info = first.get('messages', [{}])[0]
|
from_info = first.get('messages', [{}])[0]
|
||||||
user_profile = get_or_create_user(actor_id, from_info)
|
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 ──────────────────────────────────
|
# ── Get or create AgentCore session ──────────────────────────────────
|
||||||
session_id = get_or_create_session(actor_id)
|
session_id = get_or_create_session(actor_id)
|
||||||
print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}")
|
print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}")
|
||||||
|
|||||||
Reference in New Issue
Block a user