diff --git a/bsky.py b/bsky.py index e3d3f5b..9beb0a2 100644 --- a/bsky.py +++ b/bsky.py @@ -142,6 +142,8 @@ def process_mention(void_agent, atproto_client, notification_data): logger.error(f"Error fetching thread: {e}") raise + print(thread) + # Get thread context as YAML string logger.info("Converting thread to YAML string") try: diff --git a/bsky_utils.py b/bsky_utils.py index a051daa..8caffe5 100644 --- a/bsky_utils.py +++ b/bsky_utils.py @@ -234,11 +234,14 @@ def reply_to_post(client: Client, text: str, reply_to_uri: str, reply_to_cid: st facets = [] text_bytes = text.encode("UTF-8") - # Parse mentions - 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])?)" + # Parse mentions - fixed to handle @ at start of text + 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])?)" for m in re.finditer(mention_regex, text_bytes): handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix + # Adjust byte positions to account for the optional prefix + mention_start = m.start(1) + mention_end = m.end(1) try: # Resolve handle to DID using the API resolve_resp = client.app.bsky.actor.get_profile({'actor': handle}) @@ -246,8 +249,8 @@ def reply_to_post(client: Client, text: str, reply_to_uri: str, reply_to_cid: st facets.append( models.AppBskyRichtextFacet.Main( index=models.AppBskyRichtextFacet.ByteSlice( - byteStart=m.start(1), - byteEnd=m.end(1) + byteStart=mention_start, + byteEnd=mention_end ), features=[models.AppBskyRichtextFacet.Mention(did=resolve_resp.did)] ) @@ -256,16 +259,19 @@ def reply_to_post(client: Client, text: str, reply_to_uri: str, reply_to_cid: st logger.debug(f"Failed to resolve handle {handle}: {e}") continue - # Parse URLs - 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@%_\+~#//=])?)" + # Parse URLs - fixed to handle URLs at start of text + 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@%_\+~#//=])?)" for m in re.finditer(url_regex, text_bytes): url = m.group(1).decode("UTF-8") + # Adjust byte positions to account for the optional prefix + url_start = m.start(1) + url_end = m.end(1) facets.append( models.AppBskyRichtextFacet.Main( index=models.AppBskyRichtextFacet.ByteSlice( - byteStart=m.start(1), - byteEnd=m.end(1) + byteStart=url_start, + byteEnd=url_end ), features=[models.AppBskyRichtextFacet.Link(uri=url)] ) diff --git a/register_tools.py b/register_tools.py index d3191bc..3af3916 100755 --- a/register_tools.py +++ b/register_tools.py @@ -14,6 +14,18 @@ from tools.search import search_bluesky_posts, SearchArgs from tools.post import create_new_bluesky_post, PostArgs from tools.feed import get_bluesky_feed, FeedArgs from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs +from tools.defensive_memory import safe_memory_insert, safe_core_memory_replace +from pydantic import BaseModel, Field + +class SafeMemoryInsertArgs(BaseModel): + label: str = Field(..., description="Section of the memory to be edited, identified by its label") + content: str = Field(..., description="Content to insert") + insert_line: int = Field(-1, description="Line number after which to insert (-1 for end)") + +class SafeCoreMemoryReplaceArgs(BaseModel): + label: str = Field(..., description="Section of the memory to be edited") + old_content: str = Field(..., description="String to replace (must match exactly)") + new_content: str = Field(..., description="New content to replace with") load_dotenv() logging.basicConfig(level=logging.INFO) @@ -53,12 +65,18 @@ TOOL_CONFIGS = [ "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", "tags": ["memory", "blocks", "user"] }, - # { - # "func": update_user_blocks, - # "args_schema": UpdateUserBlockArgs, - # "description": "Update the content of user-specific memory blocks", - # "tags": ["memory", "blocks", "user"] - # }, + { + "func": safe_memory_insert, + "args_schema": SafeMemoryInsertArgs, + "description": "SAFE: Insert text into a memory block. Handles missing blocks by fetching from API.", + "tags": ["memory", "safe", "insert"] + }, + { + "func": safe_core_memory_replace, + "args_schema": SafeCoreMemoryReplaceArgs, + "description": "SAFE: Replace content in a memory block. Handles missing blocks by fetching from API.", + "tags": ["memory", "safe", "replace"] + }, ] diff --git a/tools/blocks.py b/tools/blocks.py index edb50f7..73e3251 100644 --- a/tools/blocks.py +++ b/tools/blocks.py @@ -69,6 +69,14 @@ def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: agent_id=str(agent_state.id), block_id=str(block.id) ) + + # STOPGAP: Also update agent_state.memory to sync in-memory state + try: + agent_state.memory.set_block(block) + print(f"[SYNC] Successfully synced block {block_label} to agent_state.memory") + except Exception as sync_error: + print(f"[SYNC] Warning: Failed to sync block to agent_state.memory: {sync_error}") + results.append(f"✓ {handle}: Block attached") logger.info(f"Successfully attached block {block_label} to agent") diff --git a/tools/defensive_memory.py b/tools/defensive_memory.py new file mode 100644 index 0000000..b0fa565 --- /dev/null +++ b/tools/defensive_memory.py @@ -0,0 +1,88 @@ +"""Defensive memory operations that handle missing blocks gracefully.""" +import os +from typing import Optional +from letta_client import Letta + + +def safe_memory_insert(agent_state: "AgentState", label: str, content: str, insert_line: int = -1) -> str: + """ + Safe version of memory_insert that handles missing blocks by fetching them from API. + + This is a stopgap solution for the dynamic block loading issue where agent_state.memory + doesn't reflect blocks that were attached via API during the same message processing cycle. + """ + try: + # Try the normal memory_insert first + from letta.functions.function_sets.base import memory_insert + return memory_insert(agent_state, label, content, insert_line) + + except KeyError as e: + if "does not exist" in str(e): + print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") + # Try to fetch the block from the API and add it to agent_state.memory + try: + client = Letta(token=os.environ["LETTA_API_KEY"]) + + # Get all blocks attached to this agent + api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) + + # Find the block we're looking for + target_block = None + for block in api_blocks: + if block.label == label: + target_block = block + break + + if target_block: + # Add it to agent_state.memory + agent_state.memory.set_block(target_block) + print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") + + # Now try the memory_insert again + from letta.functions.function_sets.base import memory_insert + return memory_insert(agent_state, label, content, insert_line) + else: + # Block truly doesn't exist + raise Exception(f"Block {label} not found in API - it may not be attached to this agent") + + except Exception as api_error: + raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") + else: + raise e # Re-raise if it's a different KeyError + + +def safe_core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> Optional[str]: + """ + Safe version of core_memory_replace that handles missing blocks. + """ + try: + # Try the normal core_memory_replace first + from letta.functions.function_sets.base import core_memory_replace + return core_memory_replace(agent_state, label, old_content, new_content) + + except KeyError as e: + if "does not exist" in str(e): + print(f"[SAFE_MEMORY] Block {label} not found in agent_state.memory, fetching from API...") + try: + client = Letta(token=os.environ["LETTA_API_KEY"]) + api_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) + + target_block = None + for block in api_blocks: + if block.label == label: + target_block = block + break + + if target_block: + agent_state.memory.set_block(target_block) + print(f"[SAFE_MEMORY] Successfully fetched and added block {label} to agent_state.memory") + + from letta.functions.function_sets.base import core_memory_replace + return core_memory_replace(agent_state, label, old_content, new_content) + else: + raise Exception(f"Block {label} not found in API - it may not be attached to this agent") + + except Exception as api_error: + raise Exception(f"Failed to fetch block {label} from API: {str(api_error)}") + else: + raise e \ No newline at end of file diff --git a/tools/post.py b/tools/post.py index 9ebd65a..ba9847d 100644 --- a/tools/post.py +++ b/tools/post.py @@ -99,12 +99,15 @@ def create_new_bluesky_post(text: List[str]) -> str: # Add facets for mentions and URLs facets = [] - # Parse mentions - 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])?)" + # Parse mentions - fixed to handle @ at start of text + 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])?)" text_bytes = post_text.encode("UTF-8") for m in re.finditer(mention_regex, text_bytes): handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix + # Adjust byte positions to account for the optional prefix + mention_start = m.start(1) + mention_end = m.end(1) try: resolve_resp = requests.get( f"{pds_host}/xrpc/com.atproto.identity.resolveHandle", @@ -115,23 +118,26 @@ def create_new_bluesky_post(text: List[str]) -> str: did = resolve_resp.json()["did"] facets.append({ "index": { - "byteStart": m.start(1), - "byteEnd": m.end(1), + "byteStart": mention_start, + "byteEnd": mention_end, }, "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}], }) except: continue - # Parse URLs - 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@%_\+~#//=])?)" + # Parse URLs - fixed to handle URLs at start of text + 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@%_\+~#//=])?)" for m in re.finditer(url_regex, text_bytes): url = m.group(1).decode("UTF-8") + # Adjust byte positions to account for the optional prefix + url_start = m.start(1) + url_end = m.end(1) facets.append({ "index": { - "byteStart": m.start(1), - "byteEnd": m.end(1), + "byteStart": url_start, + "byteEnd": url_end, }, "features": [{"$type": "app.bsky.richtext.facet#link", "uri": url}], })