Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: patch google vertex and bump version #2433

Merged
merged 6 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions examples/composio_tool_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from letta.schemas.memory import ChatMemory
from letta.schemas.sandbox_config import SandboxType
from letta.services.sandbox_config_manager import SandboxConfigManager
from letta.settings import tool_settings

"""
Setup here.
Expand All @@ -31,7 +30,7 @@


# Add sandbox env
manager = SandboxConfigManager(tool_settings)
manager = SandboxConfigManager()
# Ensure you have e2b key set
sandbox_config = manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=client.user)
manager.create_sandbox_env_var(
Expand Down
2 changes: 1 addition & 1 deletion letta/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.6.25"
__version__ = "0.6.26"

# import clients
from letta.client.client import LocalClient, RESTClient, create_client
Expand Down
30 changes: 24 additions & 6 deletions letta/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import traceback
import warnings
from abc import ABC, abstractmethod
from typing import List, Optional, Tuple, Union
from typing import Any, List, Optional, Tuple, Union

from openai.types.beta.function_tool import FunctionTool as OpenAITool

from letta.constants import (
CLI_WARNING_PREFIX,
COMPOSIO_ENTITY_ENV_VAR_KEY,
ERROR_MESSAGE_PREFIX,
FIRST_MESSAGE_ATTEMPTS,
FUNC_FAILED_HEARTBEAT_MESSAGE,
Expand All @@ -20,7 +21,11 @@
from letta.errors import ContextWindowExceededError
from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
from letta.functions.functions import get_function_from_module
from letta.functions.helpers import execute_composio_action, generate_composio_action_from_func_name
from letta.helpers import ToolRulesSolver
from letta.helpers.composio_helpers import get_composio_api_key
from letta.helpers.datetime_helpers import get_utc_time
from letta.helpers.json_helpers import json_dumps, json_loads
from letta.interface import AgentInterface
from letta.llm_api.helpers import calculate_summarizer_cutoff, get_token_counts_for_messages, is_context_overflow_error
from letta.llm_api.llm_api_tools import create
Expand Down Expand Up @@ -51,16 +56,14 @@
from letta.services.provider_manager import ProviderManager
from letta.services.step_manager import StepManager
from letta.services.tool_execution_sandbox import ToolExecutionSandbox
from letta.services.tool_manager import ToolManager
from letta.settings import summarizer_settings
from letta.streaming_interface import StreamingRefreshCLIInterface
from letta.system import get_heartbeat, get_token_limit_warning, package_function_response, package_summarize_message, package_user_message
from letta.utils import (
count_tokens,
get_friendly_error_msg,
get_tool_call_id,
get_utc_time,
json_dumps,
json_loads,
log_telemetry,
parse_json,
printd,
Expand Down Expand Up @@ -202,7 +205,7 @@ def update_memory_if_changed(self, new_memory: Memory) -> bool:

def execute_tool_and_persist_state(
self, function_name: str, function_args: dict, target_letta_tool: Tool
) -> tuple[str, Optional[SandboxRunResult]]:
) -> tuple[Any, Optional[SandboxRunResult]]:
"""
Execute tool modifications and persist the state of the agent.
Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data
Expand All @@ -228,6 +231,18 @@ def execute_tool_and_persist_state(
function_args["agent_state"] = agent_state_copy # need to attach self to arg since it's dynamically linked
function_response = callable_func(**function_args)
self.update_memory_if_changed(agent_state_copy.memory)
elif target_letta_tool.tool_type == ToolType.EXTERNAL_COMPOSIO:
action_name = generate_composio_action_from_func_name(target_letta_tool.name)
# Get entity ID from the agent_state
entity_id = None
for env_var in self.agent_state.tool_exec_environment_variables:
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
entity_id = env_var.value
# Get composio_api_key
composio_api_key = get_composio_api_key(actor=self.user, logger=self.logger)
function_response = execute_composio_action(
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
)
else:
# Parse the source code to extract function annotations
annotations = get_function_annotations_from_source(target_letta_tool.source_code, function_name)
Expand Down Expand Up @@ -460,7 +475,10 @@ def _handle_ai_response(
target_letta_tool = None
for t in self.agent_state.tools:
if t.name == function_name:
target_letta_tool = t
# This force refreshes the target_letta_tool from the database
# We only do this on name match to confirm that the agent state contains a specific tool with the right name
target_letta_tool = ToolManager().get_tool_by_name(tool_name=function_name, actor=self.user)
break

if not target_letta_tool:
error_msg = f"No function named {function_name}"
Expand Down
6 changes: 3 additions & 3 deletions letta/cli/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from prettytable.colortable import ColorTable, Themes
from tqdm import tqdm

from letta import utils
import letta.helpers.datetime_helpers

app = typer.Typer()

Expand Down Expand Up @@ -51,7 +51,7 @@ def list(arg: Annotated[ListChoice, typer.Argument]):
agent.memory.get_block("persona").value[:100] + "...",
agent.memory.get_block("human").value[:100] + "...",
",".join(source_names),
utils.format_datetime(agent.created_at),
letta.helpers.datetime_helpers.format_datetime(agent.created_at),
]
)
print(table)
Expand Down Expand Up @@ -84,7 +84,7 @@ def list(arg: Annotated[ListChoice, typer.Argument]):
source.description,
source.embedding_config.embedding_model,
source.embedding_config.embedding_dim,
utils.format_datetime(source.created_at),
letta.helpers.datetime_helpers.format_datetime(source.created_at),
]
)

Expand Down
1 change: 1 addition & 0 deletions letta/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

# minimum context window size
MIN_CONTEXT_WINDOW = 4096
DEFAULT_CONTEXT_WINDOW_SIZE = 32000

# embeddings
MAX_EMBEDDING_DIM = 4096 # maximum supported embeding size - do NOT change or else DBs will need to be reset
Expand Down
2 changes: 1 addition & 1 deletion letta/functions/function_sets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def conversation_search(self: "Agent", query: str, page: Optional[int] = 0) -> O
import math

from letta.constants import RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
from letta.utils import json_dumps
from letta.helpers.json_helpers import json_dumps

if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
page = 0
Expand Down
2 changes: 1 addition & 1 deletion letta/functions/function_sets/extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import requests

from letta.constants import MESSAGE_CHATGPT_FUNCTION_MODEL, MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE
from letta.helpers.json_helpers import json_dumps, json_loads
from letta.llm_api.llm_api_tools import create
from letta.schemas.message import Message, TextContent
from letta.utils import json_dumps, json_loads


def message_chatgpt(self, message: str):
Expand Down
53 changes: 41 additions & 12 deletions letta/functions/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,53 @@ def generate_composio_tool_wrapper(action_name: str) -> tuple[str, str]:
# Generate func name
func_name = generate_func_name_from_composio_action(action_name)

wrapper_function_str = f"""
wrapper_function_str = f"""\
def {func_name}(**kwargs):
from composio_langchain import ComposioToolSet
raise RuntimeError("Something went wrong - we should never be using the persisted source code for Composio. Please reach out to Letta team")
"""

# Compile safety check
assert_code_gen_compilable(wrapper_function_str.strip())

return func_name, wrapper_function_str.strip()


def execute_composio_action(
action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None
) -> tuple[str, str]:
import os

entity_id = os.getenv('{COMPOSIO_ENTITY_ENV_VAR_KEY}', '{DEFAULT_ENTITY_ID}')
composio_toolset = ComposioToolSet(entity_id=entity_id)
response = composio_toolset.execute_action(action='{action_name}', params=kwargs)
from composio.exceptions import (
ApiKeyNotProvidedError,
ComposioSDKError,
ConnectedAccountNotFoundError,
EnumMetadataNotFound,
EnumStringNotFound,
)
from composio_langchain import ComposioToolSet

if response["error"]:
raise RuntimeError(response["error"])
return response["data"]
"""
entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
try:
composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id)
response = composio_toolset.execute_action(action=action_name, params=args)
except ApiKeyNotProvidedError:
raise RuntimeError(
f"Composio API key is missing for action '{action_name}'. "
"Please set the sandbox environment variables either through the ADE or the API."
)
except ConnectedAccountNotFoundError:
raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
except EnumStringNotFound as e:
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
except EnumMetadataNotFound as e:
raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
except ComposioSDKError as e:
raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': " + str(e))

# Compile safety check
assert_code_gen_compilable(wrapper_function_str)
if response["error"]:
raise RuntimeError(f"Error while executing action '{action_name}': " + str(response["error"]))

return func_name, wrapper_function_str
return response["data"]


def generate_langchain_tool_wrapper(
Expand Down
21 changes: 21 additions & 0 deletions letta/helpers/composio_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from logging import Logger
from typing import Optional

from letta.schemas.user import User
from letta.services.sandbox_config_manager import SandboxConfigManager
from letta.settings import tool_settings


def get_composio_api_key(actor: User, logger: Logger) -> Optional[str]:
api_keys = SandboxConfigManager().list_sandbox_env_vars_by_key(key="COMPOSIO_API_KEY", actor=actor)
if not api_keys:
logger.warning(f"No API keys found for Composio. Defaulting to the environment variable...")
if tool_settings.composio_api_key:
return tool_settings.composio_api_key
else:
return None
else:
# TODO: Add more protections around this
# Ideally, not tied to a specific sandbox, but for now we just get the first one
# Theoretically possible for someone to have different composio api keys per sandbox
return api_keys[0].value
90 changes: 90 additions & 0 deletions letta/helpers/datetime_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import re
from datetime import datetime, timedelta, timezone

import pytz


def parse_formatted_time(formatted_time):
# parse times returned by letta.utils.get_formatted_time()
return datetime.strptime(formatted_time, "%Y-%m-%d %I:%M:%S %p %Z%z")


def datetime_to_timestamp(dt):
# convert datetime object to integer timestamp
return int(dt.timestamp())


def timestamp_to_datetime(ts):
# convert integer timestamp to datetime object
return datetime.fromtimestamp(ts)


def get_local_time_military():
# Get the current time in UTC
current_time_utc = datetime.now(pytz.utc)

# Convert to San Francisco's time zone (PST/PDT)
sf_time_zone = pytz.timezone("America/Los_Angeles")
local_time = current_time_utc.astimezone(sf_time_zone)

# You may format it as you desire
formatted_time = local_time.strftime("%Y-%m-%d %H:%M:%S %Z%z")

return formatted_time


def get_local_time_timezone(timezone="America/Los_Angeles"):
# Get the current time in UTC
current_time_utc = datetime.now(pytz.utc)

# Convert to San Francisco's time zone (PST/PDT)
sf_time_zone = pytz.timezone(timezone)
local_time = current_time_utc.astimezone(sf_time_zone)

# You may format it as you desire, including AM/PM
formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")

return formatted_time


def get_local_time(timezone=None):
if timezone is not None:
time_str = get_local_time_timezone(timezone)
else:
# Get the current time, which will be in the local timezone of the computer
local_time = datetime.now().astimezone()

# You may format it as you desire, including AM/PM
time_str = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")

return time_str.strip()


def get_utc_time() -> datetime:
"""Get the current UTC time"""
# return datetime.now(pytz.utc)
return datetime.now(timezone.utc)


def format_datetime(dt):
return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")


def validate_date_format(date_str):
"""Validate the given date string in the format 'YYYY-MM-DD'."""
try:
datetime.strptime(date_str, "%Y-%m-%d")
return True
except (ValueError, TypeError):
return False


def extract_date_from_timestamp(timestamp):
"""Extracts and returns the date from the given timestamp."""
# Extracts the date (ignoring the time and timezone)
match = re.match(r"(\d{4}-\d{2}-\d{2})", timestamp)
return match.group(1) if match else None


def is_utc_datetime(dt: datetime) -> bool:
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) == timedelta(0)
15 changes: 15 additions & 0 deletions letta/helpers/json_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import json
from datetime import datetime


def json_loads(data):
return json.loads(data, strict=False)


def json_dumps(data, indent=2):
def safe_serializer(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Type {type(obj)} not serializable")

return json.dumps(data, indent=indent, default=safe_serializer, ensure_ascii=False)
3 changes: 2 additions & 1 deletion letta/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from colorama import Fore, Style, init

from letta.constants import CLI_WARNING_PREFIX
from letta.helpers.json_helpers import json_loads
from letta.local_llm.constants import ASSISTANT_MESSAGE_CLI_SYMBOL, INNER_THOUGHTS_CLI_SYMBOL
from letta.schemas.message import Message
from letta.utils import json_loads, printd
from letta.utils import printd

init(autoreset=True)

Expand Down
2 changes: 1 addition & 1 deletion letta/llm_api/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)

from letta.errors import BedrockError, BedrockPermissionError
from letta.helpers.datetime_helpers import get_utc_time
from letta.llm_api.aws_bedrock import get_bedrock_client
from letta.llm_api.helpers import add_inner_thoughts_to_functions
from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION
Expand All @@ -39,7 +40,6 @@
from letta.services.provider_manager import ProviderManager
from letta.settings import model_settings
from letta.streaming_interface import AgentChunkStreamingInterface, AgentRefreshStreamingInterface
from letta.utils import get_utc_time

BASE_URL = "https://api.anthropic.com/v1"

Expand Down
Loading
Loading