a digital person for bluesky

Some fixes and stuff

+2
bsky.py
··· 142 142 logger.error(f"Error fetching thread: {e}") 143 143 raise 144 144 145 + print(thread) 146 + 145 147 # Get thread context as YAML string 146 148 logger.info("Converting thread to YAML string") 147 149 try:
+14 -8
bsky_utils.py
··· 234 234 facets = [] 235 235 text_bytes = text.encode("UTF-8") 236 236 237 - # Parse mentions 238 - mention_regex = rb"[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 237 + # Parse mentions - fixed to handle @ at start of text 238 + mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 239 239 240 240 for m in re.finditer(mention_regex, text_bytes): 241 241 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 242 + # Adjust byte positions to account for the optional prefix 243 + mention_start = m.start(1) 244 + mention_end = m.end(1) 242 245 try: 243 246 # Resolve handle to DID using the API 244 247 resolve_resp = client.app.bsky.actor.get_profile({'actor': handle}) ··· 246 249 facets.append( 247 250 models.AppBskyRichtextFacet.Main( 248 251 index=models.AppBskyRichtextFacet.ByteSlice( 249 - byteStart=m.start(1), 250 - byteEnd=m.end(1) 252 + byteStart=mention_start, 253 + byteEnd=mention_end 251 254 ), 252 255 features=[models.AppBskyRichtextFacet.Mention(did=resolve_resp.did)] 253 256 ) ··· 256 259 logger.debug(f"Failed to resolve handle {handle}: {e}") 257 260 continue 258 261 259 - # Parse URLs 260 - url_regex = rb"[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 262 + # Parse URLs - fixed to handle URLs at start of text 263 + url_regex = rb"(?:^|[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 261 264 262 265 for m in re.finditer(url_regex, text_bytes): 263 266 url = m.group(1).decode("UTF-8") 267 + # Adjust byte positions to account for the optional prefix 268 + url_start = m.start(1) 269 + url_end = m.end(1) 264 270 facets.append( 265 271 models.AppBskyRichtextFacet.Main( 266 272 index=models.AppBskyRichtextFacet.ByteSlice( 267 - byteStart=m.start(1), 268 - byteEnd=m.end(1) 273 + byteStart=url_start, 274 + byteEnd=url_end 269 275 ), 270 276 features=[models.AppBskyRichtextFacet.Link(uri=url)] 271 277 )
+24 -6
register_tools.py
··· 14 14 from tools.post import create_new_bluesky_post, PostArgs 15 15 from tools.feed import get_bluesky_feed, FeedArgs 16 16 from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs 17 + from tools.defensive_memory import safe_memory_insert, safe_core_memory_replace 18 + from pydantic import BaseModel, Field 19 + 20 + class SafeMemoryInsertArgs(BaseModel): 21 + label: str = Field(..., description="Section of the memory to be edited, identified by its label") 22 + content: str = Field(..., description="Content to insert") 23 + insert_line: int = Field(-1, description="Line number after which to insert (-1 for end)") 24 + 25 + class SafeCoreMemoryReplaceArgs(BaseModel): 26 + label: str = Field(..., description="Section of the memory to be edited") 27 + old_content: str = Field(..., description="String to replace (must match exactly)") 28 + new_content: str = Field(..., description="New content to replace with") 17 29 18 30 load_dotenv() 19 31 logging.basicConfig(level=logging.INFO) ··· 53 65 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", 54 66 "tags": ["memory", "blocks", "user"] 55 67 }, 56 - # { 57 - # "func": update_user_blocks, 58 - # "args_schema": UpdateUserBlockArgs, 59 - # "description": "Update the content of user-specific memory blocks", 60 - # "tags": ["memory", "blocks", "user"] 61 - # }, 68 + { 69 + "func": safe_memory_insert, 70 + "args_schema": SafeMemoryInsertArgs, 71 + "description": "SAFE: Insert text into a memory block. Handles missing blocks by fetching from API.", 72 + "tags": ["memory", "safe", "insert"] 73 + }, 74 + { 75 + "func": safe_core_memory_replace, 76 + "args_schema": SafeCoreMemoryReplaceArgs, 77 + "description": "SAFE: Replace content in a memory block. Handles missing blocks by fetching from API.", 78 + "tags": ["memory", "safe", "replace"] 79 + }, 62 80 ] 63 81 64 82
+8
tools/blocks.py
··· 69 69 agent_id=str(agent_state.id), 70 70 block_id=str(block.id) 71 71 ) 72 + 73 + # STOPGAP: Also update agent_state.memory to sync in-memory state 74 + try: 75 + agent_state.memory.set_block(block) 76 + print(f"[SYNC] Successfully synced block {block_label} to agent_state.memory") 77 + except Exception as sync_error: 78 + print(f"[SYNC] Warning: Failed to sync block to agent_state.memory: {sync_error}") 79 + 72 80 results.append(f"✓ {handle}: Block attached") 73 81 logger.info(f"Successfully attached block {block_label} to agent") 74 82
+88
tools/defensive_memory.py
··· 1 + """Defensive memory operations that handle missing blocks gracefully.""" 2 + import os 3 + from typing import Optional 4 + from letta_client import Letta 5 + 6 + 7 + def safe_memory_insert(agent_state: "AgentState", label: str, content: str, insert_line: int = -1) -> str: 8 + """ 9 + Safe version of memory_insert that handles missing blocks by fetching them from API. 10 + 11 + This is a stopgap solution for the dynamic block loading issue where agent_state.memory 12 + doesn't reflect blocks that were attached via API during the same message processing cycle. 13 + """ 14 + try: 15 + # Try the normal memory_insert first 16 + from letta.functions.function_sets.base import memory_insert 17 + return memory_insert(agent_state, label, content, insert_line) 18 + 19 + except KeyError as e: 20 + if "does not exist" in str(e): 21 + print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") 22 + # Try to fetch the block from the API and add it to agent_state.memory 23 + try: 24 + client = Letta(token=os.environ["LETTA_API_KEY"]) 25 + 26 + # Get all blocks attached to this agent 27 + api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 28 + 29 + # Find the block we're looking for 30 + target_block = None 31 + for block in api_blocks: 32 + if block.label == label: 33 + target_block = block 34 + break 35 + 36 + if target_block: 37 + # Add it to agent_state.memory 38 + agent_state.memory.set_block(target_block) 39 + print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") 40 + 41 + # Now try the memory_insert again 42 + from letta.functions.function_sets.base import memory_insert 43 + return memory_insert(agent_state, label, content, insert_line) 44 + else: 45 + # Block truly doesn't exist 46 + raise Exception(f"Block {label} not found in API - it may not be attached to this agent") 47 + 48 + except Exception as api_error: 49 + raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") 50 + else: 51 + raise e # Re-raise if it's a different KeyError 52 + 53 + 54 + def safe_core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]: 55 + """ 56 + Safe version of core_memory_replace that handles missing blocks. 57 + """ 58 + try: 59 + # Try the normal core_memory_replace first 60 + from letta.functions.function_sets.base import core_memory_replace 61 + return core_memory_replace(agent_state, label, old_content, new_content) 62 + 63 + except KeyError as e: 64 + if "does not exist" in str(e): 65 + print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") 66 + try: 67 + client = Letta(token=os.environ["LETTA_API_KEY"]) 68 + api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 69 + 70 + target_block = None 71 + for block in api_blocks: 72 + if block.label == label: 73 + target_block = block 74 + break 75 + 76 + if target_block: 77 + agent_state.memory.set_block(target_block) 78 + print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") 79 + 80 + from letta.functions.function_sets.base import core_memory_replace 81 + return core_memory_replace(agent_state, label, old_content, new_content) 82 + else: 83 + raise Exception(f"Block {label} not found in API - it may not be attached to this agent") 84 + 85 + except Exception as api_error: 86 + raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") 87 + else: 88 + raise e
+14 -8
tools/post.py
··· 99 99 # Add facets for mentions and URLs 100 100 facets = [] 101 101 102 - # Parse mentions 103 - mention_regex = rb"[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 102 + # Parse mentions - fixed to handle @ at start of text 103 + mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 104 104 text_bytes = post_text.encode("UTF-8") 105 105 106 106 for m in re.finditer(mention_regex, text_bytes): 107 107 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 108 + # Adjust byte positions to account for the optional prefix 109 + mention_start = m.start(1) 110 + mention_end = m.end(1) 108 111 try: 109 112 resolve_resp = requests.get( 110 113 f"{pds_host}/xrpc/com.atproto.identity.resolveHandle", ··· 115 118 did = resolve_resp.json()["did"] 116 119 facets.append({ 117 120 "index": { 118 - "byteStart": m.start(1), 119 - "byteEnd": m.end(1), 121 + "byteStart": mention_start, 122 + "byteEnd": mention_end, 120 123 }, 121 124 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}], 122 125 }) 123 126 except: 124 127 continue 125 128 126 - # Parse URLs 127 - url_regex = rb"[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 129 + # Parse URLs - fixed to handle URLs at start of text 130 + url_regex = rb"(?:^|[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 128 131 129 132 for m in re.finditer(url_regex, text_bytes): 130 133 url = m.group(1).decode("UTF-8") 134 + # Adjust byte positions to account for the optional prefix 135 + url_start = m.start(1) 136 + url_end = m.end(1) 131 137 facets.append({ 132 138 "index": { 133 - "byteStart": m.start(1), 134 - "byteEnd": m.end(1), 139 + "byteStart": url_start, 140 + "byteEnd": url_end, 135 141 }, 136 142 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": url}], 137 143 })