Compare commits
5 Commits
ed6577ccf9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ca5fee2c0 | ||
|
|
e77417b6cd | ||
|
|
ef5734101e | ||
|
|
8c28797bca | ||
|
|
42dbdcde9e |
@@ -13,7 +13,7 @@ def _load():
|
|||||||
ssm = boto3.client('ssm', region_name='us-east-1')
|
ssm = boto3.client('ssm', region_name='us-east-1')
|
||||||
names = list(_DEFAULTS.keys())
|
names = list(_DEFAULTS.keys())
|
||||||
try:
|
try:
|
||||||
resp = ssm.get_parameters(Names=names)
|
resp = ssm.get_parameters(Names=names, WithDecryption=True)
|
||||||
found = {p['Name']: p['Value'] for p in resp['Parameters']}
|
found = {p['Name']: p['Value'] for p in resp['Parameters']}
|
||||||
except Exception:
|
except Exception:
|
||||||
found = {}
|
found = {}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ agent-claw Runtime 1 — main assistant agent.
|
|||||||
Entrypoint for AgentCore CodeZip deployment.
|
Entrypoint for AgentCore CodeZip deployment.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from strands import Agent, tool
|
from strands import Agent, tool
|
||||||
from strands.models import BedrockModel
|
from strands.models import BedrockModel
|
||||||
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
||||||
@@ -90,6 +91,7 @@ except Exception as _e:
|
|||||||
|
|
||||||
# ── Subagent loading ──────────────────────────────────────────────────────
|
# ── Subagent loading ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
TOOL_PRESETS = {
|
TOOL_PRESETS = {
|
||||||
"aws": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp"))],
|
"aws": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp"))],
|
||||||
"coding": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp")), run_code],
|
"coding": lambda: [MCPClient(lambda: aws_iam_streamablehttp_client(config.AWS_MCP_URL, aws_service="aws-mcp")), run_code],
|
||||||
@@ -425,6 +427,131 @@ def aws_describe_service(service: str, region: str = "us-east-1") -> str:
|
|||||||
return f"Service {service} not yet implemented. Try: lambda, s3, cloudformation, dynamodb, sqs"
|
return f"Service {service} not yet implemented. Try: lambda, s3, cloudformation, dynamodb, sqs"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Goal helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
from datetime import datetime as _dt
|
||||||
|
from zoneinfo import ZoneInfo as _ZoneInfo
|
||||||
|
|
||||||
|
def _now_iso() -> str:
|
||||||
|
return _dt.now(_ZoneInfo('America/Chicago')).strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||||
|
|
||||||
|
|
||||||
|
def _read_goal() -> str | None:
|
||||||
|
"""Read GOAL.md from S3, return content or None."""
|
||||||
|
try:
|
||||||
|
return ws_tools.read_file('GOAL.md')
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_goal(content: str):
|
||||||
|
ws_tools.write_file('GOAL.md', content)
|
||||||
|
invalidate_prompt()
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_goal():
|
||||||
|
try:
|
||||||
|
_s3 = boto3.client('s3')
|
||||||
|
_s3.delete_object(Bucket=ws_tools.get_bucket(), Key='GOAL.md')
|
||||||
|
ws_tools._cache.pop('GOAL.md', None)
|
||||||
|
invalidate_prompt()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_goal_status(content: str) -> str:
|
||||||
|
"""Extract Status field from GOAL.md content."""
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.startswith('**Status:**'):
|
||||||
|
return line.split('**Status:**')[1].strip()
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_active_goal_context() -> dict | None:
|
||||||
|
"""Return goal context dict if active, else None."""
|
||||||
|
content = _read_goal()
|
||||||
|
if not content or _parse_goal_status(content) != 'active':
|
||||||
|
return None
|
||||||
|
objective = stopping = last_cp = ''
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.startswith('**Objective:**'):
|
||||||
|
objective = line.split('**Objective:**')[1].strip()
|
||||||
|
elif line.startswith('**Stopping condition:**'):
|
||||||
|
stopping = line.split('**Stopping condition:**')[1].strip()
|
||||||
|
elif line.startswith('- ['):
|
||||||
|
last_cp = line # last checkpoint line wins
|
||||||
|
return {'objective': objective, 'stopping_condition': stopping, 'last_checkpoint': last_cp}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_goal_command(prompt: str) -> str | None:
|
||||||
|
"""Handle /goal commands. Returns reply string or None if not a goal command."""
|
||||||
|
parts = prompt.split(None, 2) # ['/goal', subcommand?, rest?]
|
||||||
|
cmd = parts[1] if len(parts) > 1 else 'status'
|
||||||
|
rest = parts[2] if len(parts) > 2 else ''
|
||||||
|
|
||||||
|
if cmd == 'set':
|
||||||
|
if not rest:
|
||||||
|
return '❌ Usage: `/goal set <objective>` or `/goal set <objective> | <stopping condition>`'
|
||||||
|
if '|' in rest:
|
||||||
|
objective, stopping = [s.strip() for s in rest.split('|', 1)]
|
||||||
|
else:
|
||||||
|
objective, stopping = rest.strip(), 'not specified'
|
||||||
|
content = (
|
||||||
|
f'# Goal\n\n'
|
||||||
|
f'**Objective:** {objective}\n'
|
||||||
|
f'**Stopping condition:** {stopping}\n'
|
||||||
|
f'**Status:** active\n'
|
||||||
|
f'**Set at:** {_now_iso()}\n\n'
|
||||||
|
f'## Checkpoint log\n'
|
||||||
|
)
|
||||||
|
_write_goal(content)
|
||||||
|
return f'✅ Goal set: {objective}\nStopping condition: {stopping}'
|
||||||
|
|
||||||
|
elif cmd in ('status', '/goal'):
|
||||||
|
content = _read_goal()
|
||||||
|
if not content:
|
||||||
|
return '📋 No active goal. Use `/goal set <objective>` to set one.'
|
||||||
|
return content
|
||||||
|
|
||||||
|
elif cmd == 'checkpoint':
|
||||||
|
if not rest:
|
||||||
|
return '❌ Usage: `/goal checkpoint <note>`'
|
||||||
|
content = _read_goal()
|
||||||
|
if not content:
|
||||||
|
return '❌ No active goal to checkpoint.'
|
||||||
|
entry = f'- [{_now_iso()}] {rest}\n'
|
||||||
|
content = content.rstrip() + '\n' + entry
|
||||||
|
_write_goal(content)
|
||||||
|
return f'✅ Checkpoint added: {rest}'
|
||||||
|
|
||||||
|
elif cmd == 'pause':
|
||||||
|
content = _read_goal()
|
||||||
|
if not content:
|
||||||
|
return '❌ No active goal to pause.'
|
||||||
|
content = content.replace('**Status:** active', '**Status:** paused')
|
||||||
|
_write_goal(content)
|
||||||
|
return '⏸️ Goal paused.'
|
||||||
|
|
||||||
|
elif cmd == 'resume':
|
||||||
|
content = _read_goal()
|
||||||
|
if not content:
|
||||||
|
return '❌ No goal to resume.'
|
||||||
|
content = content.replace('**Status:** paused', '**Status:** active')
|
||||||
|
_write_goal(content)
|
||||||
|
return '▶️ Goal resumed.'
|
||||||
|
|
||||||
|
elif cmd == 'clear':
|
||||||
|
_delete_goal()
|
||||||
|
return '🗑️ Goal cleared.'
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Not a recognized subcommand — treat the whole thing as status check
|
||||||
|
content = _read_goal()
|
||||||
|
if not content:
|
||||||
|
return '📋 No active goal. Use `/goal set <objective>` to set one.'
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Module-level actor_id for tool closures (set per-invocation)
|
# Module-level actor_id for tool closures (set per-invocation)
|
||||||
@@ -528,7 +655,7 @@ async def main(payload: dict, context):
|
|||||||
system_prompt = system_prompt + '\n\n---\n\n' + ltm_block
|
system_prompt = system_prompt + '\n\n---\n\n' + ltm_block
|
||||||
|
|
||||||
system_prompt += '\nAWS tools available: call_aws (any AWS API via AWS MCP Server), aws_list_lambda_functions, aws_get_cost_and_usage, aws_describe_service. Use call_aws directly for AWS API calls — do NOT say you lack AWS access.'
|
system_prompt += '\nAWS tools available: call_aws (any AWS API via AWS MCP Server), aws_list_lambda_functions, aws_get_cost_and_usage, aws_describe_service. Use call_aws directly for AWS API calls — do NOT say you lack AWS access.'
|
||||||
system_prompt += '\n\nSubagents available — use them aggressively to save cost and improve quality:\n- aws_agent: all AWS infrastructure, cost, resource, IAM, CloudWatch queries\n- coding_agent: code writing, builds, deployments, CodeBuild/AppRunner/ECR\n- document_agent: summarize URLs, extract data from documents, process long text\nDefault to delegating; only answer directly for simple conversational responses or tasks that don\'t fit a subagent.'
|
system_prompt += '\n\nSubagents available — use them aggressively to save cost and improve quality:\n- aws_agent: all AWS infrastructure, cost, resource, IAM, CloudWatch queries\n- coding_agent: code writing, builds, deployments, CodeBuild/AppRunner/ECR\n- document_agent: summarize URLs, extract data from documents, process long text\nYou also have direct access to factcloud MCP tools (your personal knowledge graph) loaded from your MCP connections — use them directly for any factbase, factcloud, or knowledge base queries. Do NOT say you lack access to factcloud.\nDefault to delegating to subagents; only answer directly for simple conversational responses or tasks that don\'t fit a subagent.'
|
||||||
|
|
||||||
# Model: claude-sonnet-4-6 via cross-region inference
|
# Model: claude-sonnet-4-6 via cross-region inference
|
||||||
# NOTE: extended thinking disabled — causes retry/duplicate issues with streaming
|
# NOTE: extended thinking disabled — causes retry/duplicate issues with streaming
|
||||||
@@ -563,16 +690,37 @@ async def main(payload: dict, context):
|
|||||||
tools=all_tools,
|
tools=all_tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Intercept /goal commands — handle directly without LLM
|
||||||
|
prompt = payload.get('prompt', '')
|
||||||
|
if prompt.strip().startswith('/goal'):
|
||||||
|
goal_reply = _handle_goal_command(prompt.strip())
|
||||||
|
if goal_reply is not None:
|
||||||
|
yield {'data': goal_reply}
|
||||||
|
_typing_active = False
|
||||||
|
session_manager.close()
|
||||||
|
mcp_loader.close_mcp_clients(_mcp_to_close)
|
||||||
|
return
|
||||||
|
|
||||||
# Intercept heartbeat: replace bare [HEARTBEAT] with a strict-format instruction.
|
# Intercept heartbeat: replace bare [HEARTBEAT] with a strict-format instruction.
|
||||||
# Agent-runner suppresses replies that start with HEARTBEAT_OK, so only real alerts reach Telegram.
|
# Agent-runner suppresses replies that start with HEARTBEAT_OK, so only real alerts reach Telegram.
|
||||||
prompt = payload.get('prompt', '')
|
|
||||||
if prompt.strip() == '[HEARTBEAT]':
|
if prompt.strip() == '[HEARTBEAT]':
|
||||||
|
# Inject goal context into heartbeat if active
|
||||||
|
goal_ctx = _get_active_goal_context()
|
||||||
|
goal_heartbeat = ''
|
||||||
|
if goal_ctx:
|
||||||
|
goal_heartbeat = (
|
||||||
|
f' You have an active goal: "{goal_ctx["objective"]}". '
|
||||||
|
f'Stopping condition: "{goal_ctx["stopping_condition"]}". '
|
||||||
|
f'Last checkpoint: "{goal_ctx["last_checkpoint"]}". '
|
||||||
|
f'Make progress toward this goal or report blockers.'
|
||||||
|
)
|
||||||
prompt = (
|
prompt = (
|
||||||
'HEARTBEAT CHECK: Silently check for anything urgent Daniel should know about '
|
'HEARTBEAT CHECK: Silently check for anything urgent Daniel should know about '
|
||||||
'(calendar events starting within 2 hours, unread urgent emails, overdue reminders). '
|
'(calendar events starting within 2 hours, unread urgent emails, overdue reminders). '
|
||||||
'Do NOT narrate your checking process. '
|
'Do NOT narrate your checking process. '
|
||||||
'If nothing is urgent: reply with the single word HEARTBEAT_OK and nothing else. '
|
'If nothing is urgent: reply with the single word HEARTBEAT_OK and nothing else. '
|
||||||
'If something IS urgent: reply with 2-3 lines max summarising only the urgent items.'
|
'If something IS urgent: reply with 2-3 lines max summarising only the urgent items.'
|
||||||
|
+ goal_heartbeat
|
||||||
)
|
)
|
||||||
|
|
||||||
final_message = None
|
final_message = None
|
||||||
|
|||||||
@@ -45,12 +45,40 @@ def _get_oauth_token(conn: dict, actor_id: str) -> str:
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _get_m2m_token(conn: dict, actor_id: str) -> str:
|
||||||
|
"""Fetch OAuth token for oauth2_m2m (secret stored directly in record)."""
|
||||||
|
cache_key = f"{actor_id}:{conn['name']}"
|
||||||
|
cached = _token_cache.get(cache_key)
|
||||||
|
if cached and cached['expires_at'] > time.time() + 60:
|
||||||
|
return cached['token']
|
||||||
|
|
||||||
|
data = urllib.parse.urlencode({
|
||||||
|
'grant_type': 'client_credentials',
|
||||||
|
'client_id': conn['client_id'],
|
||||||
|
'client_secret': conn['client_secret'],
|
||||||
|
'scope': conn.get('scopes', conn.get('scope', '')),
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
req = urllib.request.Request(conn['token_url'], data=data,
|
||||||
|
headers={'Content-Type': 'application/x-www-form-urlencoded'})
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
body = json.loads(resp.read())
|
||||||
|
|
||||||
|
token = body['access_token']
|
||||||
|
expires_in = body.get('expires_in', 3600)
|
||||||
|
_token_cache[cache_key] = {'token': token, 'expires_at': time.time() + expires_in}
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
def _resolve_auth_headers(conn: dict, actor_id: str) -> dict:
|
def _resolve_auth_headers(conn: dict, actor_id: str) -> dict:
|
||||||
"""Resolve auth headers for a connection."""
|
"""Resolve auth headers for a connection."""
|
||||||
auth_type = conn.get('auth_type', 'none')
|
auth_type = conn.get('auth_type', 'none')
|
||||||
if auth_type == 'oauth_client_credentials':
|
if auth_type == 'oauth_client_credentials':
|
||||||
token = _get_oauth_token(conn, actor_id)
|
token = _get_oauth_token(conn, actor_id)
|
||||||
return {'Authorization': f'Bearer {token}'}
|
return {'Authorization': f'Bearer {token}'}
|
||||||
|
elif auth_type == 'oauth2_m2m':
|
||||||
|
token = _get_m2m_token(conn, actor_id)
|
||||||
|
return {'Authorization': f'Bearer {token}'}
|
||||||
elif auth_type == 'bearer':
|
elif auth_type == 'bearer':
|
||||||
token = _get_ssm_value(conn['token_ssm'])
|
token = _get_ssm_value(conn['token_ssm'])
|
||||||
return {'Authorization': f'Bearer {token}'}
|
return {'Authorization': f'Bearer {token}'}
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ def _get_base_prompt(actor_id: str = '') -> str:
|
|||||||
s3 = boto3.client('s3')
|
s3 = boto3.client('s3')
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
|
# Inject active goal at the top of context
|
||||||
|
try:
|
||||||
|
obj = s3.get_object(Bucket=bucket, Key='GOAL.md')
|
||||||
|
goal_content = obj['Body'].read().decode('utf-8')
|
||||||
|
if '**Status:** active' in goal_content:
|
||||||
|
parts.append(f'## Active Goal\n{goal_content}')
|
||||||
|
print(f'[prompt_builder] Injected GOAL.md ({len(goal_content)} bytes)')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
for fname in ['SOUL.md', 'STATUS.md']:
|
for fname in ['SOUL.md', 'STATUS.md']:
|
||||||
try:
|
try:
|
||||||
obj = s3.get_object(Bucket=bucket, Key=fname)
|
obj = s3.get_object(Bucket=bucket, Key=fname)
|
||||||
|
|||||||
35
scripts/seed_factcloud.py
Normal file
35
scripts/seed_factcloud.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Seed Daniel's factcloud MCP connection into DynamoDB."""
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
ACTOR_ID = 'telegram:8537376738'
|
||||||
|
TABLE_NAME = 'agent-claw-users'
|
||||||
|
|
||||||
|
conn = {
|
||||||
|
'name': 'factcloud',
|
||||||
|
'url': 'https://factbase-cloud-gateway-2czetaoh3u.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp',
|
||||||
|
'auth_type': 'oauth2_m2m',
|
||||||
|
'client_id': '5fo2q4fb452j3aekd55g3190i4',
|
||||||
|
'client_secret': '1e0bqs8r4jk90sbeivh96mn893mgmv96h2olvcq7m3o5gjpjc56p',
|
||||||
|
'token_url': 'https://factbase-cloud.auth.us-east-1.amazoncognito.com/oauth2/token',
|
||||||
|
'scopes': 'factbase-cloud/read factbase-cloud/write',
|
||||||
|
'enabled': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
session = boto3.Session(profile_name='ai1', region_name='us-east-1')
|
||||||
|
ddb = session.resource('dynamodb')
|
||||||
|
table = ddb.Table(TABLE_NAME)
|
||||||
|
|
||||||
|
# Get existing connections, upsert factcloud
|
||||||
|
resp = table.get_item(Key={'actor_id': ACTOR_ID})
|
||||||
|
services = resp.get('Item', {}).get('enrolled_services', {})
|
||||||
|
connections = services.get('mcp_connections', [])
|
||||||
|
connections = [c for c in connections if c['name'] != 'factcloud']
|
||||||
|
connections.append(conn)
|
||||||
|
|
||||||
|
table.update_item(
|
||||||
|
Key={'actor_id': ACTOR_ID},
|
||||||
|
UpdateExpression='SET enrolled_services.mcp_connections = :conns',
|
||||||
|
ExpressionAttributeValues={':conns': connections},
|
||||||
|
)
|
||||||
|
print(f'Seeded factcloud connection for {ACTOR_ID}')
|
||||||
Reference in New Issue
Block a user