this repo has no description

Remove centralized tools/functions.py file

Tools are now defined directly in their respective module files
(search.py, post.py, feed.py, blocks.py) rather than being
centralized in functions.py. This improves code organization
and maintainability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

-571
-571
tools/functions.py
··· 1 - """Standalone tool functions for Void Bluesky agent.""" 2 - 3 - 4 - def search_bluesky_posts(query: str, max_results: int = 25, author: str = None, sort: str = "latest") -> str: 5 - """ 6 - Search for posts on Bluesky matching the given criteria. 7 - 8 - Args: 9 - query: Search query string 10 - max_results: Maximum number of results to return (max 100) 11 - author: Filter by author handle (e.g., 'user.bsky.social') 12 - sort: Sort order: 'latest' or 'top' 13 - 14 - Returns: 15 - YAML-formatted search results with posts and metadata 16 - """ 17 - import os 18 - import yaml 19 - import requests 20 - from datetime import datetime 21 - 22 - try: 23 - # Validate inputs 24 - max_results = min(max_results, 100) 25 - if sort not in ["latest", "top"]: 26 - sort = "latest" 27 - 28 - # Build search query 29 - search_query = query 30 - if author: 31 - search_query = f"from:{author} {query}" 32 - 33 - # Get credentials from environment 34 - username = os.getenv("BSKY_USERNAME") 35 - password = os.getenv("BSKY_PASSWORD") 36 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 37 - 38 - if not username or not password: 39 - return "Error: BSKY_USERNAME and BSKY_PASSWORD environment variables must be set" 40 - 41 - # Create session 42 - session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" 43 - session_data = { 44 - "identifier": username, 45 - "password": password 46 - } 47 - 48 - try: 49 - session_response = requests.post(session_url, json=session_data, timeout=10) 50 - session_response.raise_for_status() 51 - session = session_response.json() 52 - access_token = session.get("accessJwt") 53 - 54 - if not access_token: 55 - return "Error: Failed to get access token from session" 56 - except Exception as e: 57 - return f"Error: Authentication failed. ({str(e)})" 58 - 59 - # Search posts 60 - headers = {"Authorization": f"Bearer {access_token}"} 61 - search_url = f"{pds_host}/xrpc/app.bsky.feed.searchPosts" 62 - params = { 63 - "q": search_query, 64 - "limit": max_results, 65 - "sort": sort 66 - } 67 - 68 - try: 69 - response = requests.get(search_url, headers=headers, params=params, timeout=10) 70 - response.raise_for_status() 71 - search_data = response.json() 72 - except Exception as e: 73 - return f"Error: Search failed. ({str(e)})" 74 - 75 - # Format results 76 - results = [] 77 - for post in search_data.get("posts", []): 78 - author = post.get("author", {}) 79 - record = post.get("record", {}) 80 - 81 - post_data = { 82 - "author": { 83 - "handle": author.get("handle", ""), 84 - "display_name": author.get("displayName", ""), 85 - }, 86 - "text": record.get("text", ""), 87 - "created_at": record.get("createdAt", ""), 88 - "uri": post.get("uri", ""), 89 - "cid": post.get("cid", ""), 90 - "like_count": post.get("likeCount", 0), 91 - "repost_count": post.get("repostCount", 0), 92 - "reply_count": post.get("replyCount", 0), 93 - } 94 - 95 - # Add reply info if present 96 - if "reply" in record and record["reply"]: 97 - post_data["reply_to"] = { 98 - "uri": record["reply"].get("parent", {}).get("uri", ""), 99 - "cid": record["reply"].get("parent", {}).get("cid", ""), 100 - } 101 - 102 - results.append(post_data) 103 - 104 - return yaml.dump({ 105 - "search_results": { 106 - "query": query, 107 - "author_filter": author, 108 - "sort": sort, 109 - "result_count": len(results), 110 - "posts": results 111 - } 112 - }, default_flow_style=False, sort_keys=False) 113 - 114 - except Exception as e: 115 - return f"Error searching Bluesky: {str(e)}" 116 - 117 - 118 - def post_to_bluesky(text: str) -> str: 119 - """Post a message to Bluesky.""" 120 - import os 121 - import requests 122 - from datetime import datetime, timezone 123 - 124 - try: 125 - # Validate character limit 126 - if len(text) > 300: 127 - return f"Error: Post exceeds 300 character limit (current: {len(text)} characters)" 128 - 129 - # Get credentials from environment 130 - username = os.getenv("BSKY_USERNAME") 131 - password = os.getenv("BSKY_PASSWORD") 132 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 133 - 134 - if not username or not password: 135 - return "Error: BSKY_USERNAME and BSKY_PASSWORD environment variables must be set" 136 - 137 - # Create session 138 - session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" 139 - session_data = { 140 - "identifier": username, 141 - "password": password 142 - } 143 - 144 - session_response = requests.post(session_url, json=session_data, timeout=10) 145 - session_response.raise_for_status() 146 - session = session_response.json() 147 - access_token = session.get("accessJwt") 148 - user_did = session.get("did") 149 - 150 - if not access_token or not user_did: 151 - return "Error: Failed to get access token or DID from session" 152 - 153 - # Build post record with facets for mentions and URLs 154 - now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") 155 - 156 - post_record = { 157 - "$type": "app.bsky.feed.post", 158 - "text": text, 159 - "createdAt": now, 160 - } 161 - 162 - # Add facets for mentions and URLs 163 - import re 164 - facets = [] 165 - 166 - # Parse mentions 167 - 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])?)" 168 - text_bytes = text.encode("UTF-8") 169 - 170 - for m in re.finditer(mention_regex, text_bytes): 171 - handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 172 - try: 173 - resolve_resp = requests.get( 174 - f"{pds_host}/xrpc/com.atproto.identity.resolveHandle", 175 - params={"handle": handle}, 176 - timeout=5 177 - ) 178 - if resolve_resp.status_code == 200: 179 - did = resolve_resp.json()["did"] 180 - facets.append({ 181 - "index": { 182 - "byteStart": m.start(1), 183 - "byteEnd": m.end(1), 184 - }, 185 - "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}], 186 - }) 187 - except: 188 - continue 189 - 190 - # Parse URLs 191 - 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@%_\+~#//=])?)" 192 - 193 - for m in re.finditer(url_regex, text_bytes): 194 - url = m.group(1).decode("UTF-8") 195 - facets.append({ 196 - "index": { 197 - "byteStart": m.start(1), 198 - "byteEnd": m.end(1), 199 - }, 200 - "features": [{"$type": "app.bsky.richtext.facet#link", "uri": url}], 201 - }) 202 - 203 - if facets: 204 - post_record["facets"] = facets 205 - 206 - # Create the post 207 - create_record_url = f"{pds_host}/xrpc/com.atproto.repo.createRecord" 208 - headers = {"Authorization": f"Bearer {access_token}"} 209 - 210 - create_data = { 211 - "repo": user_did, 212 - "collection": "app.bsky.feed.post", 213 - "record": post_record 214 - } 215 - 216 - post_response = requests.post(create_record_url, headers=headers, json=create_data, timeout=10) 217 - post_response.raise_for_status() 218 - result = post_response.json() 219 - 220 - post_uri = result.get("uri") 221 - handle = session.get("handle", username) 222 - rkey = post_uri.split("/")[-1] if post_uri else "" 223 - post_url = f"https://bsky.app/profile/{handle}/post/{rkey}" 224 - 225 - return f"Successfully posted to Bluesky!\nPost URL: {post_url}\nText: {text}" 226 - 227 - except Exception as e: 228 - return f"Error posting to Bluesky: {str(e)}" 229 - 230 - 231 - def get_bluesky_feed(feed_uri: str = None, max_posts: int = 25) -> str: 232 - """ 233 - Retrieve a Bluesky feed (home timeline or custom feed). 234 - 235 - Args: 236 - feed_uri: Custom feed URI (e.g., 'at://did:plc:abc/app.bsky.feed.generator/feed-name'). If not provided, returns home timeline 237 - max_posts: Maximum number of posts to retrieve (max 100) 238 - 239 - Returns: 240 - YAML-formatted feed data with posts and metadata 241 - """ 242 - import os 243 - import yaml 244 - import requests 245 - 246 - try: 247 - # Validate inputs 248 - max_posts = min(max_posts, 100) 249 - 250 - # Get credentials from environment 251 - username = os.getenv("BSKY_USERNAME") 252 - password = os.getenv("BSKY_PASSWORD") 253 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 254 - 255 - if not username or not password: 256 - return "Error: BSKY_USERNAME and BSKY_PASSWORD environment variables must be set" 257 - 258 - # Create session 259 - session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" 260 - session_data = { 261 - "identifier": username, 262 - "password": password 263 - } 264 - 265 - try: 266 - session_response = requests.post(session_url, json=session_data, timeout=10) 267 - session_response.raise_for_status() 268 - session = session_response.json() 269 - access_token = session.get("accessJwt") 270 - 271 - if not access_token: 272 - return "Error: Failed to get access token from session" 273 - except Exception as e: 274 - return f"Error: Authentication failed. ({str(e)})" 275 - 276 - # Get feed 277 - headers = {"Authorization": f"Bearer {access_token}"} 278 - 279 - if feed_uri: 280 - # Custom feed 281 - feed_url = f"{pds_host}/xrpc/app.bsky.feed.getFeed" 282 - params = { 283 - "feed": feed_uri, 284 - "limit": max_posts 285 - } 286 - feed_type = "custom" 287 - feed_name = feed_uri.split('/')[-1] if '/' in feed_uri else feed_uri 288 - else: 289 - # Home timeline 290 - feed_url = f"{pds_host}/xrpc/app.bsky.feed.getTimeline" 291 - params = { 292 - "limit": max_posts 293 - } 294 - feed_type = "home" 295 - feed_name = "timeline" 296 - 297 - try: 298 - response = requests.get(feed_url, headers=headers, params=params, timeout=10) 299 - response.raise_for_status() 300 - feed_data = response.json() 301 - except Exception as e: 302 - return f"Error: Failed to get feed. ({str(e)})" 303 - 304 - # Format posts 305 - posts = [] 306 - for item in feed_data.get("feed", []): 307 - post = item.get("post", {}) 308 - author = post.get("author", {}) 309 - record = post.get("record", {}) 310 - 311 - post_data = { 312 - "author": { 313 - "handle": author.get("handle", ""), 314 - "display_name": author.get("displayName", ""), 315 - }, 316 - "text": record.get("text", ""), 317 - "created_at": record.get("createdAt", ""), 318 - "uri": post.get("uri", ""), 319 - "cid": post.get("cid", ""), 320 - "like_count": post.get("likeCount", 0), 321 - "repost_count": post.get("repostCount", 0), 322 - "reply_count": post.get("replyCount", 0), 323 - } 324 - 325 - # Add repost info if present 326 - if "reason" in item and item["reason"]: 327 - reason = item["reason"] 328 - if reason.get("$type") == "app.bsky.feed.defs#reasonRepost": 329 - by = reason.get("by", {}) 330 - post_data["reposted_by"] = { 331 - "handle": by.get("handle", ""), 332 - "display_name": by.get("displayName", ""), 333 - } 334 - 335 - # Add reply info if present 336 - if "reply" in record and record["reply"]: 337 - parent = record["reply"].get("parent", {}) 338 - post_data["reply_to"] = { 339 - "uri": parent.get("uri", ""), 340 - "cid": parent.get("cid", ""), 341 - } 342 - 343 - posts.append(post_data) 344 - 345 - # Format response 346 - feed_result = { 347 - "feed": { 348 - "type": feed_type, 349 - "name": feed_name, 350 - "post_count": len(posts), 351 - "posts": posts 352 - } 353 - } 354 - 355 - if feed_uri: 356 - feed_result["feed"]["uri"] = feed_uri 357 - 358 - return yaml.dump(feed_result, default_flow_style=False, sort_keys=False) 359 - 360 - except Exception as e: 361 - return f"Error retrieving feed: {str(e)}" 362 - 363 - 364 - def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: 365 - """ 366 - Attach user-specific memory blocks to the agent. Creates blocks if they don't exist. 367 - 368 - Args: 369 - handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social']) 370 - agent_state: The agent state object containing agent information 371 - 372 - Returns: 373 - String with attachment results for each handle 374 - """ 375 - import os 376 - import logging 377 - from letta_client import Letta 378 - 379 - logger = logging.getLogger(__name__) 380 - 381 - try: 382 - client = Letta(token=os.environ["LETTA_API_KEY"]) 383 - results = [] 384 - 385 - # Get current blocks using the API 386 - current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 387 - current_block_labels = set() 388 - current_block_ids = [] 389 - 390 - for block in current_blocks: 391 - current_block_labels.add(block.label) 392 - current_block_ids.append(str(block.id)) 393 - 394 - # Collect new blocks to attach 395 - new_block_ids = [] 396 - 397 - for handle in handles: 398 - # Sanitize handle for block label - completely self-contained 399 - clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 400 - block_label = f"user_{clean_handle}" 401 - 402 - # Skip if already attached 403 - if block_label in current_block_labels: 404 - results.append(f"✓ {handle}: Already attached") 405 - continue 406 - 407 - # Check if block exists or create new one 408 - try: 409 - blocks = client.blocks.list(label=block_label) 410 - if blocks and len(blocks) > 0: 411 - block = blocks[0] 412 - logger.info(f"Found existing block: {block_label}") 413 - else: 414 - block = client.blocks.create( 415 - label=block_label, 416 - value=f"# User: {handle}\n\nNo information about this user yet.", 417 - limit=5000 418 - ) 419 - logger.info(f"Created new block: {block_label}") 420 - 421 - new_block_ids.append(str(block.id)) 422 - results.append(f"✓ {handle}: Block ready to attach") 423 - 424 - except Exception as e: 425 - results.append(f"✗ {handle}: Error - {str(e)}") 426 - logger.error(f"Error processing block for {handle}: {e}") 427 - 428 - # Attach all new blocks at once if there are any 429 - if new_block_ids: 430 - try: 431 - all_block_ids = current_block_ids + new_block_ids 432 - client.agents.modify( 433 - agent_id=str(agent_state.id), 434 - block_ids=all_block_ids 435 - ) 436 - logger.info(f"Successfully attached {len(new_block_ids)} new blocks to agent") 437 - except Exception as e: 438 - logger.error(f"Error attaching blocks to agent: {e}") 439 - return f"Error attaching blocks to agent: {str(e)}" 440 - 441 - return f"Attachment results:\n" + "\n".join(results) 442 - 443 - except Exception as e: 444 - logger.error(f"Error attaching user blocks: {e}") 445 - return f"Error attaching user blocks: {str(e)}" 446 - 447 - 448 - def detach_user_blocks(handles: list, agent_state: "AgentState") -> str: 449 - """ 450 - Detach user-specific memory blocks from the agent. Blocks are preserved for later use. 451 - 452 - Args: 453 - handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social']) 454 - agent_state: The agent state object containing agent information 455 - 456 - Returns: 457 - String with detachment results for each handle 458 - """ 459 - import os 460 - import logging 461 - from letta_client import Letta 462 - 463 - logger = logging.getLogger(__name__) 464 - 465 - try: 466 - client = Letta(token=os.environ["LETTA_API_KEY"]) 467 - results = [] 468 - blocks_to_remove = set() 469 - 470 - # Build mapping of block labels to IDs using the API 471 - current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 472 - block_label_to_id = {} 473 - all_block_ids = [] 474 - 475 - for block in current_blocks: 476 - block_label_to_id[block.label] = str(block.id) 477 - all_block_ids.append(str(block.id)) 478 - 479 - # Process each handle and collect blocks to remove 480 - for handle in handles: 481 - # Sanitize handle for block label - completely self-contained 482 - clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 483 - block_label = f"user_{clean_handle}" 484 - 485 - if block_label in block_label_to_id: 486 - blocks_to_remove.add(block_label_to_id[block_label]) 487 - results.append(f"✓ {handle}: Marked for detachment") 488 - else: 489 - results.append(f"✗ {handle}: Not attached") 490 - 491 - # Remove all marked blocks at once if there are any 492 - if blocks_to_remove: 493 - try: 494 - # Filter out the blocks to remove 495 - remaining_block_ids = [bid for bid in all_block_ids if bid not in blocks_to_remove] 496 - client.agents.modify( 497 - agent_id=str(agent_state.id), 498 - block_ids=remaining_block_ids 499 - ) 500 - logger.info(f"Successfully detached {len(blocks_to_remove)} blocks from agent") 501 - except Exception as e: 502 - logger.error(f"Error detaching blocks from agent: {e}") 503 - return f"Error detaching blocks from agent: {str(e)}" 504 - 505 - return f"Detachment results:\n" + "\n".join(results) 506 - 507 - except Exception as e: 508 - logger.error(f"Error detaching user blocks: {e}") 509 - return f"Error detaching user blocks: {str(e)}" 510 - 511 - 512 - def update_user_blocks(updates: list, agent_state: "AgentState" = None) -> str: 513 - """ 514 - Update the content of user-specific memory blocks. 515 - 516 - Args: 517 - updates: List of dictionaries with 'handle' and 'content' keys 518 - agent_state: The agent state object (optional, used for consistency) 519 - 520 - Returns: 521 - String with update results for each handle 522 - """ 523 - import os 524 - import logging 525 - from letta_client import Letta 526 - 527 - logger = logging.getLogger(__name__) 528 - 529 - try: 530 - client = Letta(token=os.environ["LETTA_API_KEY"]) 531 - results = [] 532 - 533 - for update in updates: 534 - handle = update.get('handle') 535 - new_content = update.get('content') 536 - 537 - if not handle or not new_content: 538 - results.append(f"✗ Invalid update: missing handle or content") 539 - continue 540 - 541 - # Sanitize handle for block label - completely self-contained 542 - clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 543 - block_label = f"user_{clean_handle}" 544 - 545 - try: 546 - # Find the block 547 - blocks = client.blocks.list(label=block_label) 548 - if not blocks or len(blocks) == 0: 549 - results.append(f"✗ {handle}: Block not found - use attach_user_blocks first") 550 - continue 551 - 552 - block = blocks[0] 553 - 554 - # Update block content 555 - updated_block = client.blocks.modify( 556 - block_id=str(block.id), 557 - value=new_content 558 - ) 559 - 560 - preview = new_content[:100] + "..." if len(new_content) > 100 else new_content 561 - results.append(f"✓ {handle}: Updated - {preview}") 562 - 563 - except Exception as e: 564 - results.append(f"✗ {handle}: Error - {str(e)}") 565 - logger.error(f"Error updating block for {handle}: {e}") 566 - 567 - return f"Update results:\n" + "\n".join(results) 568 - 569 - except Exception as e: 570 - logger.error(f"Error updating user blocks: {e}") 571 - return f"Error updating user blocks: {str(e)}"