1"""Block management tools for user-specific memory blocks."""
2from pydantic import BaseModel, Field
3from typing import List, Dict, Any
4import logging
5
6def get_letta_client():
7 """Get a Letta client using configuration."""
8 try:
9 from config_loader import get_letta_config
10 from letta_client import Letta
11 config = get_letta_config()
12 return Letta(token=config['api_key'], timeout=config['timeout'])
13 except (ImportError, FileNotFoundError, KeyError):
14 # Fallback to environment variable
15 import os
16 from letta_client import Letta
17 return Letta(token=os.environ["LETTA_API_KEY"])
18
19
20class AttachUserBlocksArgs(BaseModel):
21 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])")
22
23
24class DetachUserBlocksArgs(BaseModel):
25 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])")
26
27
28class UserNoteAppendArgs(BaseModel):
29 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
30 note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\n- Cameron is a person')")
31
32
33class UserNoteReplaceArgs(BaseModel):
34 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
35 old_text: str = Field(..., description="Text to find and replace in the user's memory block")
36 new_text: str = Field(..., description="Text to replace the old_text with")
37
38
39class UserNoteSetArgs(BaseModel):
40 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
41 content: str = Field(..., description="Complete content to set for the user's memory block")
42
43
44class UserNoteViewArgs(BaseModel):
45 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')")
46
47
48
49def attach_user_blocks(handles: list, agent_state: "AgentState") -> str:
50 """
51 Attach user-specific memory blocks to the agent. Creates blocks if they don't exist.
52
53 Args:
54 handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])
55 agent_state: The agent state object containing agent information
56
57 Returns:
58 String with attachment results for each handle
59 """
60 logger = logging.getLogger(__name__)
61
62 handles = list(set(handles))
63
64 try:
65 client = get_letta_client()
66 results = []
67
68 # Get current blocks using the API
69 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
70 current_block_labels = set()
71
72 for block in current_blocks:
73 current_block_labels.add(block.label)
74
75 for handle in handles:
76 # Sanitize handle for block label - completely self-contained
77 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
78 block_label = f"user_{clean_handle}"
79
80 # Skip if already attached
81 if block_label in current_block_labels:
82 results.append(f"✓ {handle}: Already attached")
83 continue
84
85 # Check if block exists or create new one
86 try:
87 blocks = client.blocks.list(label=block_label)
88 if blocks and len(blocks) > 0:
89 block = blocks[0]
90 logger.debug(f"Found existing block: {block_label}")
91 else:
92 block = client.blocks.create(
93 label=block_label,
94 value=f"# User: {handle}\n\nNo information about this user yet.",
95 limit=5000
96 )
97 logger.info(f"Created new block: {block_label}")
98
99 # Attach block atomically
100 client.agents.blocks.attach(
101 agent_id=str(agent_state.id),
102 block_id=str(block.id)
103 )
104
105 results.append(f"✓ {handle}: Block attached")
106 logger.debug(f"Successfully attached block {block_label} to agent")
107
108 except Exception as e:
109 results.append(f"✗ {handle}: Error - {str(e)}")
110 logger.error(f"Error processing block for {handle}: {e}")
111
112 return f"Attachment results:\n" + "\n".join(results)
113
114 except Exception as e:
115 logger.error(f"Error attaching user blocks: {e}")
116 raise Exception(f"Error attaching user blocks: {str(e)}")
117
118
119def detach_user_blocks(handles: list, agent_state: "AgentState") -> str:
120 """
121 Detach user-specific memory blocks from the agent. Blocks are preserved for later use.
122
123 Args:
124 handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])
125 agent_state: The agent state object containing agent information
126
127 Returns:
128 String with detachment results for each handle
129 """
130 logger = logging.getLogger(__name__)
131
132 try:
133 client = get_letta_client()
134 results = []
135
136 # Build mapping of block labels to IDs using the API
137 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
138 block_label_to_id = {}
139
140 for block in current_blocks:
141 block_label_to_id[block.label] = str(block.id)
142
143 # Process each handle and detach atomically
144 for handle in handles:
145 # Sanitize handle for block label - completely self-contained
146 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
147 block_label = f"user_{clean_handle}"
148
149 if block_label in block_label_to_id:
150 try:
151 # Detach block atomically
152 client.agents.blocks.detach(
153 agent_id=str(agent_state.id),
154 block_id=block_label_to_id[block_label]
155 )
156 results.append(f"✓ {handle}: Detached")
157 logger.debug(f"Successfully detached block {block_label} from agent")
158 except Exception as e:
159 results.append(f"✗ {handle}: Error during detachment - {str(e)}")
160 logger.error(f"Error detaching block {block_label}: {e}")
161 else:
162 results.append(f"✗ {handle}: Not attached")
163
164 return f"Detachment results:\n" + "\n".join(results)
165
166 except Exception as e:
167 logger.error(f"Error detaching user blocks: {e}")
168 raise Exception(f"Error detaching user blocks: {str(e)}")
169
170
171def user_note_append(handle: str, note: str, agent_state: "AgentState") -> str:
172 """
173 Append a note to a user's memory block. Creates the block if it doesn't exist.
174
175 Args:
176 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org')
177 note: Note to append to the user's memory block
178 agent_state: The agent state object containing agent information
179
180 Returns:
181 String confirming the note was appended
182 """
183 logger = logging.getLogger(__name__)
184
185 try:
186 client = get_letta_client()
187
188 # Sanitize handle for block label
189 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
190 block_label = f"user_{clean_handle}"
191
192 # Check if block exists
193 blocks = client.blocks.list(label=block_label)
194
195 if blocks and len(blocks) > 0:
196 # Block exists, append to it
197 block = blocks[0]
198 current_value = block.value
199 new_value = current_value + note
200
201 # Update the block
202 client.blocks.modify(
203 block_id=str(block.id),
204 value=new_value
205 )
206 logger.info(f"Appended note to existing block: {block_label}")
207 return f"✓ Appended note to {handle}'s memory block"
208
209 else:
210 # Block doesn't exist, create it with the note
211 initial_value = f"# User: {handle}\n\n{note}"
212 block = client.blocks.create(
213 label=block_label,
214 value=initial_value,
215 limit=5000
216 )
217 logger.info(f"Created new block with note: {block_label}")
218
219 # Check if block needs to be attached to agent
220 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
221 current_block_labels = {block.label for block in current_blocks}
222
223 if block_label not in current_block_labels:
224 # Attach the new block to the agent
225 client.agents.blocks.attach(
226 agent_id=str(agent_state.id),
227 block_id=str(block.id)
228 )
229 logger.info(f"Attached new block to agent: {block_label}")
230 return f"✓ Created and attached {handle}'s memory block with note"
231 else:
232 return f"✓ Created {handle}'s memory block with note"
233
234 except Exception as e:
235 logger.error(f"Error appending note to user block: {e}")
236 raise Exception(f"Error appending note to user block: {str(e)}")
237
238
239def user_note_replace(handle: str, old_text: str, new_text: str, agent_state: "AgentState") -> str:
240 """
241 Replace text in a user's memory block.
242
243 Args:
244 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org')
245 old_text: Text to find and replace
246 new_text: Text to replace the old_text with
247 agent_state: The agent state object containing agent information
248
249 Returns:
250 String confirming the text was replaced
251 """
252 logger = logging.getLogger(__name__)
253
254 try:
255 client = get_letta_client()
256
257 # Sanitize handle for block label
258 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
259 block_label = f"user_{clean_handle}"
260
261 # Check if block exists
262 blocks = client.blocks.list(label=block_label)
263
264 if not blocks or len(blocks) == 0:
265 raise Exception(f"No memory block found for user: {handle}")
266
267 block = blocks[0]
268 current_value = block.value
269
270 # Check if old_text exists in the block
271 if old_text not in current_value:
272 raise Exception(f"Text '{old_text}' not found in {handle}'s memory block")
273
274 # Replace the text
275 new_value = current_value.replace(old_text, new_text)
276
277 # Update the block
278 client.blocks.modify(
279 block_id=str(block.id),
280 value=new_value
281 )
282 logger.info(f"Replaced text in block: {block_label}")
283 return f"✓ Replaced text in {handle}'s memory block"
284
285 except Exception as e:
286 logger.error(f"Error replacing text in user block: {e}")
287 raise Exception(f"Error replacing text in user block: {str(e)}")
288
289
290def user_note_set(handle: str, content: str, agent_state: "AgentState") -> str:
291 """
292 Set the complete content of a user's memory block.
293
294 Args:
295 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org')
296 content: Complete content to set for the memory block
297 agent_state: The agent state object containing agent information
298
299 Returns:
300 String confirming the content was set
301 """
302 logger = logging.getLogger(__name__)
303
304 try:
305 client = get_letta_client()
306
307 # Sanitize handle for block label
308 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
309 block_label = f"user_{clean_handle}"
310
311 # Check if block exists
312 blocks = client.blocks.list(label=block_label)
313
314 if blocks and len(blocks) > 0:
315 # Block exists, update it
316 block = blocks[0]
317 client.blocks.modify(
318 block_id=str(block.id),
319 value=content
320 )
321 logger.info(f"Set content for existing block: {block_label}")
322 return f"✓ Set content for {handle}'s memory block"
323
324 else:
325 # Block doesn't exist, create it
326 block = client.blocks.create(
327 label=block_label,
328 value=content,
329 limit=5000
330 )
331 logger.info(f"Created new block with content: {block_label}")
332
333 # Check if block needs to be attached to agent
334 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id))
335 current_block_labels = {block.label for block in current_blocks}
336
337 if block_label not in current_block_labels:
338 # Attach the new block to the agent
339 client.agents.blocks.attach(
340 agent_id=str(agent_state.id),
341 block_id=str(block.id)
342 )
343 logger.info(f"Attached new block to agent: {block_label}")
344 return f"✓ Created and attached {handle}'s memory block"
345 else:
346 return f"✓ Created {handle}'s memory block"
347
348 except Exception as e:
349 logger.error(f"Error setting user block content: {e}")
350 raise Exception(f"Error setting user block content: {str(e)}")
351
352
353def user_note_view(handle: str, agent_state: "AgentState") -> str:
354 """
355 View the content of a user's memory block.
356
357 Args:
358 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org')
359 agent_state: The agent state object containing agent information
360
361 Returns:
362 String containing the user's memory block content
363 """
364 logger = logging.getLogger(__name__)
365
366 try:
367 client = get_letta_client()
368
369 # Sanitize handle for block label
370 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
371 block_label = f"user_{clean_handle}"
372
373 # Check if block exists
374 blocks = client.blocks.list(label=block_label)
375
376 if not blocks or len(blocks) == 0:
377 return f"No memory block found for user: {handle}"
378
379 block = blocks[0]
380 logger.info(f"Retrieved content for block: {block_label}")
381
382 return f"Memory block for {handle}:\n\n{block.value}"
383
384 except Exception as e:
385 logger.error(f"Error viewing user block: {e}")
386 raise Exception(f"Error viewing user block: {str(e)}")
387
388