multi-tenant Phase 1: user registry + per-user memory

- CDK: add agent-claw-users DynamoDB table (actor_id PK, RETAIN policy)
- CDK: grant agent-runner read/write on users table; add USERS_TABLE_NAME env
- CDK: fix cdk.json app field (was object, must be command string)
- CDK: add UsersTableName output
- agent-runner: get_or_create_user() auto-registers users on first contact
  (stores display_name, telegram_username, created_at, allowed)
- agent-runner: pass user_profile in AgentCore payload
- prompt_builder: split base prompt (cached) from per-user context (injected per-call)
  removes USER.md/MEMORY.md from shared load; user name/username injected dynamically
- main.py: extract user_profile from payload, build user_context string for prompt
This commit is contained in:
daniel
2026-05-06 20:36:22 -05:00
parent 732b00fb66
commit 893c110729
16 changed files with 726 additions and 501 deletions

View File

@@ -140,8 +140,16 @@ def main(payload: dict, context) -> dict:
region_name='us-east-1',
)
# Build system prompt (cached across warm invocations)
system_prompt = build_system_prompt()
# Build system prompt — base cached, user context injected per-invocation
user_profile = payload.get('user_profile', {})
user_context = ''
if user_profile:
name = user_profile.get('display_name', '')
username = user_profile.get('telegram_username', '')
user_context = f'Name: {name}'
if username:
user_context += f'\nTelegram username: @{username}'
system_prompt = build_system_prompt(user_context=user_context)
# Model: claude-sonnet-4-6 via cross-region inference
model = BedrockModel(

View File

@@ -1,28 +1,35 @@
import os
import boto3
# Cache: built once per warm session
_system_prompt: str | None = None
# Cache: built once per warm session (shared base only)
_base_prompt: str | None = None
def build_system_prompt() -> str:
"""Build system prompt from S3 workspace files (cached for warm session)."""
global _system_prompt
if _system_prompt is not None:
return _system_prompt
def build_system_prompt(user_context: str = '') -> str:
"""Build system prompt from S3 workspace files + optional per-user context."""
base = _get_base_prompt()
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
bucket = os.environ.get('WORKSPACE_BUCKET_NAME', '') or 'agent-claw-workspace-495395224548'
print(f'[prompt_builder] Loading from bucket: {bucket!r}')
if not bucket:
print('[prompt_builder] WARNING: WORKSPACE_BUCKET_NAME not set!')
_system_prompt = 'You are a helpful personal assistant.'
return _system_prompt
_base_prompt = 'You are a helpful personal assistant.'
return _base_prompt
s3 = boto3.client('s3')
parts = []
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'USER.md', 'MEMORY.md', 'TOOLS.md']:
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'TOOLS.md']:
try:
obj = s3.get_object(Bucket=bucket, Key=fname)
content = obj['Body'].read().decode('utf-8')
@@ -33,12 +40,12 @@ def build_system_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.')
_system_prompt = '\n\n---\n\n'.join(parts)
print(f'[prompt_builder] System prompt built: {len(_system_prompt)} chars')
return _system_prompt
_base_prompt = '\n\n---\n\n'.join(parts)
print(f'[prompt_builder] Base prompt built: {len(_base_prompt)} chars')
return _base_prompt
def invalidate_prompt() -> None:
"""Force rebuild of system prompt on next invocation (call after workspace write)."""
global _system_prompt
_system_prompt = None
global _base_prompt
_base_prompt = None