from typing import List, Dict, Any from smolagents import CodeAgent, InferenceClientModel, tool import os import json import sys from pathlib import Path from datetime import datetime def find_chrome_bookmarks_file(): """ Automatically detects the Chrome bookmarks file path based on the OS. Returns the absolute path string. """ home = Path.home() if sys.platform.startswith("win"): # Windows base = home / "AppData" / "Local" / "Google" / "Chrome" / "User Data" / "Default" elif sys.platform.startswith("darwin"): # macOS base = home / "Library" / "Application Support" / "Google" / "Chrome" / "Default" else: # Linux base = home / ".config" / "google-chrome" / "Default" bookmark_file = base / "Bookmarks" if not bookmark_file.exists(): raise FileNotFoundError(f"Cannot find Chrome Bookmarks file at: {bookmark_file}") return str(bookmark_file) def find_folder_by_name(node, target_name): """ Recursively searches for a folder with the given name. """ if not isinstance(node, dict): return None if node.get("type") == "folder" and node.get("name") == target_name: return node # Search in children for child in node.get("children", []): found = find_folder_by_name(child, target_name) if found: return found return None def extract_bookmarks_from_folder(node, bookmark_list): """ Recursively extracts all bookmarks from a folder node. """ if not isinstance(node, dict): return if node.get("type") == "url": # Individual bookmark bookmark_data = { "title": node.get("name", ""), "url": node.get("url", ""), "date_added": node.get("date_added", ""), "date_modified": node.get("date_modified", ""), "id": node.get("id", ""), } bookmark_list.append(bookmark_data) elif node.get("type") == "folder": # Folder, process children children = node.get("children", []) for child in children: extract_bookmarks_from_folder(child, bookmark_list) def get_cache_file_path(): """Returns the path for the bookmark cache file.""" # Create data folder in the root repository data_dir = Path("data") data_dir.mkdir(exist_ok=True) return str(data_dir / "ai_bookmarks_cache.json") def load_cache(): """Loads the bookmark cache from JSON file.""" cache_file = get_cache_file_path() if os.path.exists(cache_file): try: with open(cache_file, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print(f"Error loading cache: {e}") return {"bookmarks": [], "last_updated": None} def save_cache(cache_data): """Saves the bookmark cache to JSON file.""" cache_file = get_cache_file_path() try: with open(cache_file, "w", encoding="utf-8") as f: json.dump(cache_data, f, indent=2, ensure_ascii=False) return True except Exception as e: print(f"Error saving cache: {e}") return False @tool def update_ai_bookmarks_cache() -> Dict[str, Any]: """ Extracts bookmarks from Chrome's 'AI ressources' folder and saves them to the data/ai_bookmarks_cache.json file. This creates a local cache that avoids direct interaction with Chrome's raw JSON file for subsequent operations. Returns: Dictionary with update status and bookmark count. """ try: # Find Chrome bookmarks file bookmarks_file = find_chrome_bookmarks_file() # Load Chrome bookmarks with open(bookmarks_file, "r", encoding="utf-8") as f: data = json.load(f) # Find the 'AI ressources' folder ai_folder = None roots = data.get("roots", {}) for key in ("bookmark_bar", "other", "synced"): if key in roots: ai_folder = find_folder_by_name(roots[key], "AI ressources") if ai_folder: break if not ai_folder: return {"status": "error", "message": "AI ressources folder not found in bookmarks"} # Extract bookmarks from AI ressources folder bookmarks = [] extract_bookmarks_from_folder(ai_folder, bookmarks) # Create cache data with metadata cache_data = { "bookmarks": bookmarks, "last_updated": datetime.now().isoformat(), "folder_name": "AI ressources", "total_count": len(bookmarks), } # Save to cache if save_cache(cache_data): return { "status": "success", "message": f"Successfully updated cache with {len(bookmarks)} bookmarks", "count": len(bookmarks), } else: return {"status": "error", "message": "Failed to save cache"} except Exception as e: return {"status": "error", "message": f"Error updating cache: {str(e)}"} @tool def get_latest_ai_bookmarks(n: int = 10) -> List[Dict[str, Any]]: """ Gets the n latest bookmarks from the AI ressources cache. Args: n: Number of latest bookmarks to return (default: 10) Returns: List of the latest bookmarks with metadata. """ cache = load_cache() bookmarks = cache.get("bookmarks", []) if not bookmarks: return [] # Sort by date_added (newest first) if available try: sorted_bookmarks = sorted(bookmarks, key=lambda x: int(x.get("date_added", "0")), reverse=True) except (ValueError, TypeError): # If sorting fails, return as is sorted_bookmarks = bookmarks return sorted_bookmarks[:n] @tool def search_ai_bookmarks(query: str) -> List[Dict[str, Any]]: """ Search AI ressources bookmarks for entries matching a query. Args: query: Search term to find in bookmark titles or URLs. Returns: List of matching bookmarks. """ cache = load_cache() bookmarks = cache.get("bookmarks", []) if not bookmarks: return [] query_lower = query.lower() matching_bookmarks = [] for bookmark in bookmarks: title = bookmark.get("title", "").lower() url = bookmark.get("url", "").lower() if query_lower in title or query_lower in url: matching_bookmarks.append(bookmark) return matching_bookmarks @tool def get_bookmark_statistics() -> Dict[str, Any]: """ Gets statistics about the AI ressources bookmarks cache. Returns: Dictionary with various statistics about the bookmarks. """ cache = load_cache() bookmarks = cache.get("bookmarks", []) if not bookmarks: return {"total_count": 0, "last_updated": None} # Calculate statistics total_count = len(bookmarks) domains = {} for bookmark in bookmarks: url = bookmark.get("url", "") try: from urllib.parse import urlparse domain = urlparse(url).netloc domains[domain] = domains.get(domain, 0) + 1 except (ValueError, AttributeError): pass # Get top domains top_domains = sorted(domains.items(), key=lambda x: x[1], reverse=True)[:5] return { "total_count": total_count, "last_updated": cache.get("last_updated"), "top_domains": top_domains, "unique_domains": len(domains), } @tool def get_all_ai_bookmarks() -> List[Dict[str, Any]]: """ Gets all bookmarks from the AI ressources cache. Returns: List of all cached bookmarks. """ cache = load_cache() return cache.get("bookmarks", []) @tool def filter_bookmarks_by_domain(domain: str) -> List[Dict[str, Any]]: """ Filters AI ressources bookmarks by domain. Args: domain: Domain name to filter by (e.g., 'github.com') Returns: List of bookmarks from the specified domain. """ cache = load_cache() bookmarks = cache.get("bookmarks", []) if not bookmarks: return [] domain_lower = domain.lower() filtered_bookmarks = [] for bookmark in bookmarks: url = bookmark.get("url", "") try: from urllib.parse import urlparse bookmark_domain = urlparse(url).netloc.lower() if domain_lower in bookmark_domain: filtered_bookmarks.append(bookmark) except (ValueError, AttributeError): pass return filtered_bookmarks @tool def get_cache_info() -> Dict[str, Any]: """ Gets information about the bookmark cache file. Returns: Dictionary with cache file information. """ cache_file = get_cache_file_path() cache = load_cache() info = { "cache_file_path": cache_file, "cache_exists": os.path.exists(cache_file), "last_updated": cache.get("last_updated"), "bookmark_count": len(cache.get("bookmarks", [])), "folder_name": cache.get("folder_name", "Unknown"), } if os.path.exists(cache_file): stat = os.stat(cache_file) info["file_size_bytes"] = stat.st_size info["file_modified"] = datetime.fromtimestamp(stat.st_mtime).isoformat() return info # Instantiate the Bookmarks CodeAgent with enhanced tools bookmarks_agent = CodeAgent( model=InferenceClientModel( provider="nebius", token=os.environ["HF_TOKEN"], ), tools=[ update_ai_bookmarks_cache, get_latest_ai_bookmarks, search_ai_bookmarks, get_bookmark_statistics, get_all_ai_bookmarks, filter_bookmarks_by_domain, get_cache_info, ], name="bookmarks_agent", description="Specialized agent for Chrome bookmarks operations, focusing on AI ressources folder. Extracts bookmarks from Chrome and caches them in data/ai_bookmarks_cache.json to avoid direct interaction with Chrome's raw JSON. Provides search, filtering, statistics, and cache management for AI-related bookmarks.", max_steps=10, additional_authorized_imports=["json", "datetime", "urllib.parse", "pathlib"], # Reduce verbosity stream_outputs=False, max_print_outputs_length=300, )