+2
bsky.py
+2
bsky.py
+14
-8
bsky_utils.py
+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
+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
+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
+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
+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
})