+74
-6
bsky.py
+74
-6
bsky.py
···
95
os.makedirs("agents", exist_ok=True)
96
97
# Export agent data
98
-
logger.info(f"Exporting agent {agent.id}...")
99
agent_data = client.agents.export_file(agent_id=agent.id)
100
101
# Save timestamped archive copy
···
231
raise
232
233
# Get thread context as YAML string
234
-
logger.info("Converting thread to YAML string")
235
try:
236
thread_context = thread_to_yaml_string(thread)
237
-
logger.info(f"Thread context generated, length: {len(thread_context)} characters")
238
239
# Create a more informative preview by extracting meaningful content
240
lines = thread_context.split('\n')
···
327
# Log condensed chunk info
328
if hasattr(chunk, 'message_type'):
329
if chunk.message_type == 'reasoning_message':
330
-
logger.info(f"๐ง Reasoning: {chunk.reasoning[:150]}...")
331
elif chunk.message_type == 'tool_call_message':
332
-
logger.info(f"๐ง Tool call: {chunk.tool_call.name}({chunk.tool_call.arguments[:150]}...)")
333
elif chunk.message_type == 'tool_return_message':
334
-
logger.info(f"๐ Tool result: {chunk.name} - {chunk.status}")
335
elif chunk.message_type == 'assistant_message':
336
logger.info(f"๐ฌ Assistant: {chunk.content[:150]}...")
337
else:
···
95
os.makedirs("agents", exist_ok=True)
96
97
# Export agent data
98
+
logger.info(f"Exporting agent {agent.id}. This takes some time...")
99
agent_data = client.agents.export_file(agent_id=agent.id)
100
101
# Save timestamped archive copy
···
231
raise
232
233
# Get thread context as YAML string
234
+
logger.debug("Converting thread to YAML string")
235
try:
236
thread_context = thread_to_yaml_string(thread)
237
+
logger.debug(f"Thread context generated, length: {len(thread_context)} characters")
238
239
# Create a more informative preview by extracting meaningful content
240
lines = thread_context.split('\n')
···
327
# Log condensed chunk info
328
if hasattr(chunk, 'message_type'):
329
if chunk.message_type == 'reasoning_message':
330
+
# Show full reasoning without truncation
331
+
logger.info(f"๐ง Reasoning: {chunk.reasoning}")
332
elif chunk.message_type == 'tool_call_message':
333
+
# Parse tool arguments for better display
334
+
tool_name = chunk.tool_call.name
335
+
try:
336
+
args = json.loads(chunk.tool_call.arguments)
337
+
# Format based on tool type
338
+
if tool_name == 'bluesky_reply':
339
+
messages = args.get('messages', [args.get('message', '')])
340
+
lang = args.get('lang', 'en-US')
341
+
if messages and isinstance(messages, list):
342
+
preview = messages[0][:100] + "..." if len(messages[0]) > 100 else messages[0]
343
+
msg_count = f" ({len(messages)} msgs)" if len(messages) > 1 else ""
344
+
logger.info(f"๐ง Tool call: {tool_name} โ \"{preview}\"{msg_count} [lang: {lang}]")
345
+
else:
346
+
logger.info(f"๐ง Tool call: {tool_name}({chunk.tool_call.arguments[:150]}...)")
347
+
elif tool_name == 'archival_memory_search':
348
+
query = args.get('query', 'unknown')
349
+
logger.info(f"๐ง Tool call: {tool_name} โ query: \"{query}\"")
350
+
elif tool_name == 'update_block':
351
+
label = args.get('label', 'unknown')
352
+
value_preview = str(args.get('value', ''))[:50] + "..." if len(str(args.get('value', ''))) > 50 else str(args.get('value', ''))
353
+
logger.info(f"๐ง Tool call: {tool_name} โ {label}: \"{value_preview}\"")
354
+
else:
355
+
# Generic display for other tools
356
+
args_str = ', '.join(f"{k}={v}" for k, v in args.items() if k != 'request_heartbeat')
357
+
if len(args_str) > 150:
358
+
args_str = args_str[:150] + "..."
359
+
logger.info(f"๐ง Tool call: {tool_name}({args_str})")
360
+
except:
361
+
# Fallback to original format if parsing fails
362
+
logger.info(f"๐ง Tool call: {tool_name}({chunk.tool_call.arguments[:150]}...)")
363
elif chunk.message_type == 'tool_return_message':
364
+
# Enhanced tool result logging
365
+
tool_name = chunk.name
366
+
status = chunk.status
367
+
368
+
if status == 'success':
369
+
# Try to show meaningful result info based on tool type
370
+
if hasattr(chunk, 'tool_return') and chunk.tool_return:
371
+
result_str = str(chunk.tool_return)
372
+
if tool_name == 'archival_memory_search':
373
+
# Count number of results if it looks like a list
374
+
if result_str.startswith('[') and result_str.endswith(']'):
375
+
try:
376
+
results = json.loads(result_str)
377
+
logger.info(f"๐ Tool result: {tool_name} โ Found {len(results)} memory entries")
378
+
except:
379
+
logger.info(f"๐ Tool result: {tool_name} โ {result_str[:100]}...")
380
+
else:
381
+
logger.info(f"๐ Tool result: {tool_name} โ {result_str[:100]}...")
382
+
elif tool_name == 'bluesky_reply':
383
+
logger.info(f"๐ Tool result: {tool_name} โ Reply posted successfully")
384
+
elif tool_name == 'update_block':
385
+
logger.info(f"๐ Tool result: {tool_name} โ Memory block updated")
386
+
else:
387
+
# Generic success with preview
388
+
preview = result_str[:100] + "..." if len(result_str) > 100 else result_str
389
+
logger.info(f"๐ Tool result: {tool_name} โ {preview}")
390
+
else:
391
+
logger.info(f"๐ Tool result: {tool_name} โ")
392
+
elif status == 'error':
393
+
# Show error details
394
+
error_preview = ""
395
+
if hasattr(chunk, 'tool_return') and chunk.tool_return:
396
+
error_str = str(chunk.tool_return)
397
+
error_preview = error_str[:100] + "..." if len(error_str) > 100 else error_str
398
+
logger.info(f"๐ Tool result: {tool_name} โ Error: {error_preview}")
399
+
else:
400
+
logger.info(f"๐ Tool result: {tool_name} โ Error occurred")
401
+
else:
402
+
logger.info(f"๐ Tool result: {tool_name} - {status}")
403
elif chunk.message_type == 'assistant_message':
404
logger.info(f"๐ฌ Assistant: {chunk.content[:150]}...")
405
else:
+7
-1
register_tools.py
+7
-1
register_tools.py
···
13
from tools.search import search_bluesky_posts, SearchArgs
14
from tools.post import create_new_bluesky_post, PostArgs
15
from tools.feed import get_bluesky_feed, FeedArgs
16
-
from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs
17
from tools.reply import bluesky_reply, ReplyArgs
18
from tools.halt import halt_activity, HaltArgs
19
···
54
"args_schema": DetachUserBlocksArgs,
55
"description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.",
56
"tags": ["memory", "blocks", "user"]
57
},
58
{
59
"func": bluesky_reply,
···
13
from tools.search import search_bluesky_posts, SearchArgs
14
from tools.post import create_new_bluesky_post, PostArgs
15
from tools.feed import get_bluesky_feed, FeedArgs
16
+
from tools.blocks import attach_user_blocks, detach_user_blocks, user_note_append, AttachUserBlocksArgs, DetachUserBlocksArgs, UserNoteAppendArgs
17
from tools.reply import bluesky_reply, ReplyArgs
18
from tools.halt import halt_activity, HaltArgs
19
···
54
"args_schema": DetachUserBlocksArgs,
55
"description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.",
56
"tags": ["memory", "blocks", "user"]
57
+
},
58
+
{
59
+
"func": user_note_append,
60
+
"args_schema": UserNoteAppendArgs,
61
+
"description": "Append a note to a user's memory block. Creates the block if it doesn't exist.",
62
+
"tags": ["memory", "blocks", "user", "append"]
63
},
64
{
65
"func": bluesky_reply,
+88
tools/blocks.py
+88
tools/blocks.py
···
11
handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])")
12
13
14
+
class UserNoteAppendArgs(BaseModel):
15
+
handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
16
+
note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\n- Cameron is a person')")
17
+
18
+
19
+
class UserNoteReplaceArgs(BaseModel):
20
+
handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
21
+
old_text: str = Field(..., description="Text to find and replace in the user's memory block")
22
+
new_text: str = Field(..., description="Text to replace the old_text with")
23
+
24
+
25
+
class UserNoteSetArgs(BaseModel):
26
+
handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
27
+
content: str = Field(..., description="Complete content to set for the user's memory block")
28
+
29
+
30
31
def attach_user_blocks(handles: list, agent_state: "AgentState") -> str:
32
"""
···
156
raise Exception(f"Error detaching user blocks: {str(e)}")
157
158
159
+
def user_note_append(handle: str, note: str, agent_state: "AgentState") -> str:
160
+
"""
161
+
Append a note to a user's memory block. Creates the block if it doesn't exist.
162
+
163
+
Args:
164
+
handle: User Bluesky handle (e.g., 'cameron.pfiffer.org')
165
+
note: Note to append to the user's memory block
166
+
agent_state: The agent state object containing agent information
167
+
168
+
Returns:
169
+
String confirming the note was appended
170
+
"""
171
+
import os
172
+
import logging
173
+
from letta_client import Letta
174
+
175
+
logger = logging.getLogger(__name__)
176
+
177
+
try:
178
+
client = Letta(token=os.environ["LETTA_API_KEY"])
179
+
180
+
# Sanitize handle for block label
181
+
clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
182
+
block_label = f"user_{clean_handle}"
183
+
184
+
# Check if block exists
185
+
blocks = client.blocks.list(label=block_label)
186
+
187
+
if blocks and len(blocks) > 0:
188
+
# Block exists, append to it
189
+
block = blocks[0]
190
+
current_value = block.value
191
+
new_value = current_value + note
192
+
193
+
# Update the block
194
+
client.blocks.modify(
195
+
block_id=str(block.id),
196
+
value=new_value
197
+
)
198
+
logger.info(f"Appended note to existing block: {block_label}")
199
+
return f"โ Appended note to {handle}'s memory block"
200
+
201
+
else:
202
+
# Block doesn't exist, create it with the note
203
+
initial_value = f"# User: {handle}\n\n{note}"
204
+
block = client.blocks.create(
205
+
label=block_label,
206
+
value=initial_value,
207
+
limit=5000
208
+
)
209
+
logger.info(f"Created new block with note: {block_label}")
210
+
211
+
# Check if block needs to be attached to agent
212
+
current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
213
+
current_block_labels = {block.label for block in current_blocks}
214
+
215
+
if block_label not in current_block_labels:
216
+
# Attach the new block to the agent
217
+
client.agents.blocks.attach(
218
+
agent_id=str(agent_state.id),
219
+
block_id=str(block.id)
220
+
)
221
+
logger.info(f"Attached new block to agent: {block_label}")
222
+
return f"โ Created and attached {handle}'s memory block with note"
223
+
else:
224
+
return f"โ Created {handle}'s memory block with note"
225
+
226
+
except Exception as e:
227
+
logger.error(f"Error appending note to user block: {e}")
228
+
raise Exception(f"Error appending note to user block: {str(e)}")
229
+
230
+