+206
-5
bsky.py
+206
-5
bsky.py
···
108
108
# Skip git operations flag
109
109
SKIP_GIT = False
110
110
111
+
# Synthesis message tracking
112
+
last_synthesis_time = time.time()
113
+
111
114
def export_agent_state(client, agent, skip_git=False):
112
115
"""Export agent state to agent_archive/ (timestamped) and agents/ (current)."""
113
116
try:
···
314
317
unique_handles = list(all_handles)
315
318
316
319
logger.debug(f"Found {len(unique_handles)} unique handles in thread: {unique_handles}")
320
+
321
+
# Check if any handles are in known_bots list
322
+
from tools.bot_detection import check_known_bots, should_respond_to_bot_thread, CheckKnownBotsArgs
323
+
import json
324
+
325
+
try:
326
+
# Check for known bots in thread
327
+
bot_check_result = check_known_bots(unique_handles, void_agent)
328
+
bot_check_data = json.loads(bot_check_result)
329
+
330
+
if bot_check_data.get("bot_detected", False):
331
+
detected_bots = bot_check_data.get("detected_bots", [])
332
+
logger.info(f"Bot detected in thread: {detected_bots}")
333
+
334
+
# Decide whether to respond (10% chance)
335
+
if not should_respond_to_bot_thread():
336
+
logger.info(f"Skipping bot thread (90% skip rate). Detected bots: {detected_bots}")
337
+
# Return False to keep in queue for potential later processing
338
+
return False
339
+
else:
340
+
logger.info(f"Responding to bot thread (10% response rate). Detected bots: {detected_bots}")
341
+
else:
342
+
logger.debug("No known bots detected in thread")
343
+
344
+
except Exception as bot_check_error:
345
+
logger.warning(f"Error checking for bots: {bot_check_error}")
346
+
# Continue processing if bot check fails
317
347
318
348
# Attach user blocks before agent call
319
349
attached_handles = []
···
1202
1232
logger.error(f"Error processing notifications: {e}")
1203
1233
1204
1234
1235
+
def send_synthesis_message(client: Letta, agent_id: str, atproto_client=None) -> None:
1236
+
"""
1237
+
Send a synthesis message to the agent every 10 minutes.
1238
+
This prompts the agent to synthesize its recent experiences.
1239
+
1240
+
Args:
1241
+
client: Letta client
1242
+
agent_id: Agent ID to send synthesis to
1243
+
atproto_client: Optional AT Protocol client for posting synthesis results
1244
+
"""
1245
+
try:
1246
+
logger.info("🧠 Sending synthesis prompt to agent")
1247
+
1248
+
# Send synthesis message with streaming to show tool use
1249
+
message_stream = client.agents.messages.create_stream(
1250
+
agent_id=agent_id,
1251
+
messages=[{"role": "user", "content": "Synthesize."}],
1252
+
stream_tokens=False,
1253
+
max_steps=100
1254
+
)
1255
+
1256
+
# Track synthesis content for potential posting
1257
+
synthesis_posts = []
1258
+
ack_note = None
1259
+
1260
+
# Process the streaming response
1261
+
for chunk in message_stream:
1262
+
if hasattr(chunk, 'message_type'):
1263
+
if chunk.message_type == 'reasoning_message':
1264
+
if SHOW_REASONING:
1265
+
print("\n◆ Reasoning")
1266
+
print(" ─────────")
1267
+
for line in chunk.reasoning.split('\n'):
1268
+
print(f" {line}")
1269
+
elif chunk.message_type == 'tool_call_message':
1270
+
tool_name = chunk.tool_call.name
1271
+
try:
1272
+
args = json.loads(chunk.tool_call.arguments)
1273
+
if tool_name == 'archival_memory_search':
1274
+
query = args.get('query', 'unknown')
1275
+
log_with_panel(f"query: \"{query}\"", f"Tool call: {tool_name}", "blue")
1276
+
elif tool_name == 'archival_memory_insert':
1277
+
content = args.get('content', '')
1278
+
log_with_panel(content[:200] + "..." if len(content) > 200 else content, f"Tool call: {tool_name}", "blue")
1279
+
elif tool_name == 'update_block':
1280
+
label = args.get('label', 'unknown')
1281
+
value_preview = str(args.get('value', ''))[:100] + "..." if len(str(args.get('value', ''))) > 100 else str(args.get('value', ''))
1282
+
log_with_panel(f"{label}: \"{value_preview}\"", f"Tool call: {tool_name}", "blue")
1283
+
elif tool_name == 'annotate_ack':
1284
+
note = args.get('note', '')
1285
+
if note:
1286
+
ack_note = note
1287
+
log_with_panel(f"note: \"{note[:100]}...\"" if len(note) > 100 else f"note: \"{note}\"", f"Tool call: {tool_name}", "blue")
1288
+
elif tool_name == 'add_post_to_bluesky_reply_thread':
1289
+
text = args.get('text', '')
1290
+
synthesis_posts.append(text)
1291
+
log_with_panel(f"text: \"{text[:100]}...\"" if len(text) > 100 else f"text: \"{text}\"", f"Tool call: {tool_name}", "blue")
1292
+
else:
1293
+
args_str = ', '.join(f"{k}={v}" for k, v in args.items() if k != 'request_heartbeat')
1294
+
if len(args_str) > 150:
1295
+
args_str = args_str[:150] + "..."
1296
+
log_with_panel(args_str, f"Tool call: {tool_name}", "blue")
1297
+
except:
1298
+
log_with_panel(chunk.tool_call.arguments[:150] + "...", f"Tool call: {tool_name}", "blue")
1299
+
elif chunk.message_type == 'tool_return_message':
1300
+
if chunk.status == 'success':
1301
+
log_with_panel("Success", f"Tool result: {chunk.name} ✓", "green")
1302
+
else:
1303
+
log_with_panel("Error", f"Tool result: {chunk.name} ✗", "red")
1304
+
elif chunk.message_type == 'assistant_message':
1305
+
print("\n▶ Synthesis Response")
1306
+
print(" ──────────────────")
1307
+
for line in chunk.content.split('\n'):
1308
+
print(f" {line}")
1309
+
1310
+
if str(chunk) == 'done':
1311
+
break
1312
+
1313
+
logger.info("🧠 Synthesis message processed successfully")
1314
+
1315
+
# Handle synthesis acknowledgments if we have an atproto client
1316
+
if atproto_client and ack_note:
1317
+
try:
1318
+
result = bsky_utils.create_synthesis_ack(atproto_client, ack_note)
1319
+
if result:
1320
+
logger.info(f"✓ Created synthesis acknowledgment: {ack_note[:50]}...")
1321
+
else:
1322
+
logger.warning("Failed to create synthesis acknowledgment")
1323
+
except Exception as e:
1324
+
logger.error(f"Error creating synthesis acknowledgment: {e}")
1325
+
1326
+
# Handle synthesis posts if any were generated
1327
+
if atproto_client and synthesis_posts:
1328
+
try:
1329
+
for post_text in synthesis_posts:
1330
+
cleaned_text = bsky_utils.remove_outside_quotes(post_text)
1331
+
response = bsky_utils.send_post(atproto_client, cleaned_text)
1332
+
if response:
1333
+
logger.info(f"✓ Posted synthesis content: {cleaned_text[:50]}...")
1334
+
else:
1335
+
logger.warning(f"Failed to post synthesis content: {cleaned_text[:50]}...")
1336
+
except Exception as e:
1337
+
logger.error(f"Error posting synthesis content: {e}")
1338
+
1339
+
except Exception as e:
1340
+
logger.error(f"Error sending synthesis message: {e}")
1341
+
1342
+
1205
1343
def periodic_user_block_cleanup(client: Letta, agent_id: str) -> None:
1206
1344
"""
1207
1345
Detach all user blocks from the agent to prevent memory bloat.
···
1252
1390
# --rich option removed as we now use simple text formatting
1253
1391
parser.add_argument('--reasoning', action='store_true', help='Display reasoning in panels and set reasoning log level to INFO')
1254
1392
parser.add_argument('--cleanup-interval', type=int, default=10, help='Run user block cleanup every N cycles (default: 10, 0 to disable)')
1393
+
parser.add_argument('--synthesis-interval', type=int, default=600, help='Send synthesis message every N seconds (default: 600 = 10 minutes, 0 to disable)')
1394
+
parser.add_argument('--synthesis-only', action='store_true', help='Run in synthesis-only mode (only send synthesis messages, no notification processing)')
1255
1395
args = parser.parse_args()
1256
1396
1257
1397
# Configure logging based on command line arguments
···
1335
1475
logger.info(" - Queue files will not be deleted")
1336
1476
logger.info(" - Notifications will not be marked as seen")
1337
1477
print("\n")
1478
+
1479
+
# Check for synthesis-only mode
1480
+
SYNTHESIS_ONLY = args.synthesis_only
1481
+
if SYNTHESIS_ONLY:
1482
+
logger.info("=== RUNNING IN SYNTHESIS-ONLY MODE ===")
1483
+
logger.info(" - Only synthesis messages will be sent")
1484
+
logger.info(" - No notification processing")
1485
+
logger.info(" - No Bluesky client needed")
1486
+
print("\n")
1338
1487
"""Main bot loop that continuously monitors for notifications."""
1339
1488
global start_time
1340
1489
start_time = time.time()
···
1361
1510
else:
1362
1511
logger.warning("Agent has no tools registered!")
1363
1512
1364
-
# Initialize Bluesky client
1365
-
atproto_client = bsky_utils.default_login()
1366
-
logger.info("Connected to Bluesky")
1513
+
# Initialize Bluesky client (needed for both notification processing and synthesis acks/posts)
1514
+
if not SYNTHESIS_ONLY:
1515
+
atproto_client = bsky_utils.default_login()
1516
+
logger.info("Connected to Bluesky")
1517
+
else:
1518
+
# In synthesis-only mode, still connect for acks and posts (unless in test mode)
1519
+
if not args.test:
1520
+
atproto_client = bsky_utils.default_login()
1521
+
logger.info("Connected to Bluesky (for synthesis acks/posts)")
1522
+
else:
1523
+
atproto_client = None
1524
+
logger.info("Skipping Bluesky connection (test mode)")
1367
1525
1368
-
# Main loop
1526
+
# Configure intervals
1527
+
CLEANUP_INTERVAL = args.cleanup_interval
1528
+
SYNTHESIS_INTERVAL = args.synthesis_interval
1529
+
1530
+
# Synthesis-only mode
1531
+
if SYNTHESIS_ONLY:
1532
+
if SYNTHESIS_INTERVAL <= 0:
1533
+
logger.error("Synthesis-only mode requires --synthesis-interval > 0")
1534
+
return
1535
+
1536
+
logger.info(f"Starting synthesis-only mode, interval: {SYNTHESIS_INTERVAL} seconds ({SYNTHESIS_INTERVAL/60:.1f} minutes)")
1537
+
1538
+
while True:
1539
+
try:
1540
+
# Send synthesis message immediately on first run
1541
+
logger.info("🧠 Sending synthesis message")
1542
+
send_synthesis_message(CLIENT, void_agent.id, atproto_client)
1543
+
1544
+
# Wait for next interval
1545
+
logger.info(f"Waiting {SYNTHESIS_INTERVAL} seconds until next synthesis...")
1546
+
sleep(SYNTHESIS_INTERVAL)
1547
+
1548
+
except KeyboardInterrupt:
1549
+
logger.info("=== SYNTHESIS MODE STOPPED BY USER ===")
1550
+
break
1551
+
except Exception as e:
1552
+
logger.error(f"Error in synthesis loop: {e}")
1553
+
logger.info(f"Sleeping for {SYNTHESIS_INTERVAL} seconds due to error...")
1554
+
sleep(SYNTHESIS_INTERVAL)
1555
+
1556
+
# Normal mode with notification processing
1369
1557
logger.info(f"Starting notification monitoring, checking every {FETCH_NOTIFICATIONS_DELAY_SEC} seconds")
1370
1558
1371
1559
cycle_count = 0
1372
-
CLEANUP_INTERVAL = args.cleanup_interval
1373
1560
1374
1561
if CLEANUP_INTERVAL > 0:
1375
1562
logger.info(f"User block cleanup enabled every {CLEANUP_INTERVAL} cycles")
1376
1563
else:
1377
1564
logger.info("User block cleanup disabled")
1378
1565
1566
+
if SYNTHESIS_INTERVAL > 0:
1567
+
logger.info(f"Synthesis messages enabled every {SYNTHESIS_INTERVAL} seconds ({SYNTHESIS_INTERVAL/60:.1f} minutes)")
1568
+
else:
1569
+
logger.info("Synthesis messages disabled")
1570
+
1379
1571
while True:
1380
1572
try:
1381
1573
cycle_count += 1
1382
1574
process_notifications(void_agent, atproto_client, TESTING_MODE)
1575
+
1576
+
# Check if synthesis interval has passed
1577
+
if SYNTHESIS_INTERVAL > 0:
1578
+
current_time = time.time()
1579
+
global last_synthesis_time
1580
+
if current_time - last_synthesis_time >= SYNTHESIS_INTERVAL:
1581
+
logger.info(f"⏰ {SYNTHESIS_INTERVAL/60:.1f} minutes have passed, triggering synthesis")
1582
+
send_synthesis_message(CLIENT, void_agent.id, atproto_client)
1583
+
last_synthesis_time = current_time
1383
1584
1384
1585
# Run periodic cleanup every N cycles
1385
1586
if CLEANUP_INTERVAL > 0 and cycle_count % CLEANUP_INTERVAL == 0: