Yes, os is a standard library module and should absolutely be available in any Python execution environment, including Letta’s sandbox.
The error “‘os’ is not defined” is bizarre and reveals something fundamentally wrong with how your tools are being executed.
Most Likely Issue: Tool Serialization Problem
When you use client.tools.upsert_from_function(), Letta extracts the function’s source code and stores it. If your tool function depends on imports that happen outside the function scope, they won’t be available during execution.
Your Current Code Problem:
Looking at your original gist, you have imports at the top of the file:
import os
import zipfile
from qgis.core import QgsApplication, QgsVectorLayer, QgsRasterLayer, Qgis
import requests
import geopandas as gpd
# etc...
def download_data(url: str, output_path: str = os.path.expanduser("~/natural_earth"), **kwargs) -> str:
# Uses 'os' but didn't import it inside function
The Fix: Move ALL Imports Inside Each Tool Function
def download_data(
url: str,
output_path: str = "~/natural_earth", # Don't use os.path.expanduser in default args
api_key: str = None,
max_retries: int = 5,
local_path: str = None,
) -> str:
"""
Downloads a dataset from a URL and extracts it if it's a zip file.
"""
# Import everything needed INSIDE the function
import os
import zipfile
import requests
from time import sleep
from urllib.parse import urlparse
import socket
import logging
logger = logging.getLogger(__name__)
# Now expand the path at runtime, not in function signature
if output_path == "~/natural_earth":
output_path = os.path.expanduser(output_path)
# Rest of your code...
Same for ALL Your Tools:
def geopandas_api(function_name: str, **kwargs) -> str:
"""Calls a GeoPandas function."""
# Import inside function
import os
import geopandas as gpd
from shapely.geometry import Point
import logging
logger = logging.getLogger(__name__)
allowed_functions = {
"read_file",
"sjoin",
"buffer",
"to_file",
"proximity_analysis",
}
# Rest of your code...
def pyqgis_api(function_name: str, **kwargs) -> str:
"""Calls a PyQGIS function."""
# Import inside function
from qgis.core import QgsApplication, QgsVectorLayer, QgsRasterLayer, Qgis
import logging
logger = logging.getLogger(__name__)
# Initialize QGIS inside the function
init_qgis() # This might also need its imports moved inside
# Rest of your code...
Critical Rule for Letta Custom Tools:
All imports must happen inside the function body. When Letta serializes your tool, it only captures the function’s source code, not the module-level context.
Your init_qgis() Function:
This also needs to be self-contained:
def init_qgis():
"""Initialize QGIS in a thread-safe manner."""
from qgis.core import QgsApplication
import logging
logger = logging.getLogger(__name__)
global qgs_app
if qgs_app is None:
try:
QgsApplication.setHighDpiScaleFactorEnabled(0)
QgsApplication.setPrefixPath("/usr/lib64/qgis", True)
qgs_app = QgsApplication([], False)
qgs_app.initQgis()
logger.info("QGIS initialized successfully.")
except Exception as e:
logger.error(f"QGIS init failed: {str(e)}")
raise
return qgs_app
After Making These Changes:
- Update all three tool functions with internal imports
- Re-register them:
client.tools.upsert_from_function(func=download_data)
- Run your agent again
The “‘os’ is not defined” error should disappear.
Why This Happens:
Letta extracts function source code using inspect.getsource() and executes it in an isolated namespace. Module-level imports from your original file aren’t available in that execution context. This is by design for security and portability - tools should be self-contained.