diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 18948961..620b793f 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -8,7 +8,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
-
+
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
@@ -17,7 +17,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v3
-
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -38,4 +38,3 @@ jobs:
letta/letta:latest
memgpt/letta:${{ env.CURRENT_VERSION }}
memgpt/letta:latest
-
diff --git a/.github/workflows/letta-code-sync.yml b/.github/workflows/letta-code-sync.yml
new file mode 100644
index 00000000..391047b4
--- /dev/null
+++ b/.github/workflows/letta-code-sync.yml
@@ -0,0 +1,19 @@
+name: Sync Code
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ notify:
+ runs-on: ubuntu-latest
+ if: ${{ !contains(github.event.head_commit.message, '[sync-skip]') }}
+ steps:
+ - name: Trigger repository_dispatch
+ run: |
+ curl -X POST \
+ -H "Authorization: token ${{ secrets.SYNC_PAT }}" \
+ -H "Accept: application/vnd.github.v3+json" \
+ https://api.github.com/repos/letta-ai/letta-cloud/dispatches \
+ -d '{"event_type":"oss-update"}'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0d8f16f7..c7b7d3a5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,18 +19,46 @@ Now, let's bring your new playground to your local machine.
git clone https://github.com/your-username/letta.git
```
-### 🧩 Install Dependencies
+### 🧩 Install dependencies & configure environment
+
+#### Install poetry and dependencies
First, install Poetry using [the official instructions here](https://python-poetry.org/docs/#installation).
-Once Poetry is installed, navigate to the Letta directory and install the Letta project with Poetry:
+Once Poetry is installed, navigate to the letta directory and install the Letta project with Poetry:
```shell
-cd Letta
+cd letta
poetry shell
poetry install --all-extras
```
+#### Setup PostgreSQL environment (optional)
+
+If you are planning to develop letta connected to PostgreSQL database, you need to take the following actions.
+If you are not planning to use PostgreSQL database, you can skip to the step which talks about [running letta](#running-letta-with-poetry).
+
+Assuming you have a running PostgreSQL instance, first you need to create the user, database and ensure the pgvector
+extension is ready. Here are sample steps for a case where user and database name is letta and assumes no password is set:
+
+```shell
+createuser letta
+createdb letta --owner=letta
+psql -d letta -c 'CREATE EXTENSION IF NOT EXISTS vector'
+```
+Setup the environment variable to tell letta code to contact PostgreSQL database:
+```shell
+export LETTA_PG_URI="postgresql://${POSTGRES_USER:-letta}:${POSTGRES_PASSWORD:-letta}@localhost:5432/${POSTGRES_DB:-letta}"
+```
+
+After this you need to prep the database with initial content. You can use alembic upgrade to populate the initial
+contents from template test data. Please ensure to activate poetry environment using `poetry shell`.
+```shell
+alembic upgrade head
+```
+
+#### Running letta with poetry
Now when you want to use `letta`, make sure you first activate the `poetry` environment using poetry shell:
+
```shell
$ poetry shell
(pyletta-py3.12) $ letta run
diff --git a/README.md b/README.md
index 9ccb2a50..a46cddc9 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
Letta (previously MemGPT)
-**☄️ New release: Letta Agent Development Environment (_read more [here](#-access-the-letta-ade-agent-development-environment)_) ☄️**
+**☄️ New release: Letta Agent Development Environment (_read more [here](#-access-the-ade-agent-development-environment)_) ☄️**
@@ -23,7 +23,7 @@
-[Homepage](https://letta.com) // [Documentation](https://docs.letta.com) // [ADE](https://app.letta.com) // [Letta Cloud](https://forms.letta.com/early-access)
+[Homepage](https://letta.com) // [Documentation](https://docs.letta.com) // [ADE](https://docs.letta.com/agent-development-environment) // [Letta Cloud](https://forms.letta.com/early-access)
@@ -80,12 +80,12 @@ docker run \
Once the Letta server is running, you can access it via port `8283` (e.g. sending REST API requests to `http://localhost:8283/v1`). You can also connect your server to the Letta ADE to access and manage your agents in a web interface.
-### 👾 Access the [Letta ADE (Agent Development Environment)](https://app.letta.com)
+### 👾 Access the ADE (Agent Development Environment)
> [!NOTE]
-> The Letta ADE is a graphical user interface for creating, deploying, interacting and observing with your Letta agents.
->
-> For example, if you're running a Letta server to power an end-user application (such as a customer support chatbot), you can use the ADE to test, debug, and observe the agents in your server. You can also use the ADE as a general chat interface to interact with your Letta agents.
+> For a guided tour of the ADE, watch our [ADE walkthrough on YouTube](https://www.youtube.com/watch?v=OzSCFR0Lp5s), or read our [blog post](https://www.letta.com/blog/introducing-the-agent-development-environment) and [developer docs](https://docs.letta.com/agent-development-environment).
+
+The Letta ADE is a graphical user interface for creating, deploying, interacting and observing with your Letta agents. For example, if you're running a Letta server to power an end-user application (such as a customer support chatbot), you can use the ADE to test, debug, and observe the agents in your server. You can also use the ADE as a general chat interface to interact with your Letta agents.
diff --git a/letta/__init__.py b/letta/__init__.py
index dd79db95..33e2b673 100644
--- a/letta/__init__.py
+++ b/letta/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.6.9"
+__version__ = "0.6.13"
# import clients
diff --git a/letta/client/client.py b/letta/client/client.py
index 7bfae1af..35b0c6f6 100644
--- a/letta/client/client.py
+++ b/letta/client/client.py
@@ -442,7 +442,8 @@ class RESTClient(AbstractClient):
def __init__(
self,
base_url: str,
- token: str,
+ token: Optional[str] = None,
+ password: Optional[str] = None,
api_prefix: str = "v1",
debug: bool = False,
default_llm_config: Optional[LLMConfig] = None,
@@ -458,11 +459,18 @@ class RESTClient(AbstractClient):
default_llm_config (Optional[LLMConfig]): The default LLM configuration.
default_embedding_config (Optional[EmbeddingConfig]): The default embedding configuration.
headers (Optional[Dict]): The additional headers for the REST API.
+ token (Optional[str]): The token for the REST API when using managed letta service.
+ password (Optional[str]): The password for the REST API when using self hosted letta service.
"""
super().__init__(debug=debug)
self.base_url = base_url
self.api_prefix = api_prefix
- self.headers = {"accept": "application/json", "authorization": f"Bearer {token}"}
+ if token:
+ self.headers = {"accept": "application/json", "Authorization": f"Bearer {token}"}
+ elif password:
+ self.headers = {"accept": "application/json", "X-BARE-PASSWORD": f"password {password}"}
+ else:
+ self.headers = {"accept": "application/json"}
if headers:
self.headers.update(headers)
self._default_llm_config = default_llm_config
diff --git a/letta/orm/job_usage_statistics.py b/letta/orm/job_usage_statistics.py
new file mode 100644
index 00000000..0a355d69
--- /dev/null
+++ b/letta/orm/job_usage_statistics.py
@@ -0,0 +1,30 @@
+from typing import TYPE_CHECKING, Optional
+
+from sqlalchemy import ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from letta.orm.sqlalchemy_base import SqlalchemyBase
+
+if TYPE_CHECKING:
+ from letta.orm.job import Job
+
+
+class JobUsageStatistics(SqlalchemyBase):
+ """Tracks usage statistics for jobs, with future support for per-step tracking."""
+
+ __tablename__ = "job_usage_statistics"
+
+ id: Mapped[int] = mapped_column(primary_key=True, doc="Unique identifier for the usage statistics entry")
+ job_id: Mapped[str] = mapped_column(
+ ForeignKey("jobs.id", ondelete="CASCADE"), nullable=False, doc="ID of the job these statistics belong to"
+ )
+ step_id: Mapped[Optional[str]] = mapped_column(
+ nullable=True, doc="ID of the specific step within the job (for future per-step tracking)"
+ )
+ completion_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens generated by the agent")
+ prompt_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens in the prompt")
+ total_tokens: Mapped[int] = mapped_column(default=0, doc="Total number of tokens processed by the agent")
+ step_count: Mapped[int] = mapped_column(default=0, doc="Number of steps taken by the agent")
+
+ # Relationship back to the job
+ job: Mapped["Job"] = relationship("Job", back_populates="usage_statistics")
diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py
index d02d87d1..dcc3ca71 100644
--- a/letta/server/rest_api/routers/v1/agents.py
+++ b/letta/server/rest_api/routers/v1/agents.py
@@ -560,6 +560,9 @@ async def process_message_background(
)
server.job_manager.update_job_by_id(job_id=job_id, job_update=job_update, actor=actor)
+ # Add job usage statistics
+ server.job_manager.add_job_usage(job_id=job_id, usage=result.usage, actor=actor)
+
except Exception as e:
# Update job status to failed
job_update = JobUpdate(
diff --git a/poetry.lock b/poetry.lock
index 2904a3b6..a509287c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -4507,18 +4507,15 @@ cli = ["click (>=5.0)"]
[[package]]
name = "python-multipart"
-version = "0.0.9"
+version = "0.0.19"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
- {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
+ {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"},
+ {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"},
]
-[package.extras]
-dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
-
[[package]]
name = "pytz"
version = "2023.4"
@@ -5172,19 +5169,18 @@ tornado = ["tornado (>=6)"]
[[package]]
name = "setuptools"
-version = "68.2.2"
+version = "70.3.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
- {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
- {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
+ {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"},
+ {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"},
]
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "shellingham"
@@ -6293,4 +6289,4 @@ tests = ["wikipedia"]
[metadata]
lock-version = "2.0"
python-versions = "<3.14,>=3.10"
-content-hash = "2f552617ff233fe8b07bdec4dc1679935df30030046984962b69ebe625717815"
+content-hash = "bfb2713daba35ef8c78ee1b568c35afe3f1d0c247ea58a58a079e1fb4d984c10"
diff --git a/pyproject.toml b/pyproject.toml
index 730edd9e..b952b84c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,7 @@
[tool.poetry]
name = "letta"
-version = "0.6.9"
+
+version = "0.6.13"
packages = [
{include = "letta"},
]
@@ -21,7 +22,7 @@ questionary = "^2.0.1"
pytz = "^2023.3.post1"
tqdm = "^4.66.1"
black = {extras = ["jupyter"], version = "^24.2.0"}
-setuptools = "^68.2.2"
+setuptools = "^70"
datasets = { version = "^2.14.6", optional = true}
prettytable = "^3.9.0"
pgvector = { version = "^0.2.3", optional = true }
@@ -47,7 +48,7 @@ qdrant-client = {version="^1.9.1", optional = true}
python-box = "^7.1.1"
sqlmodel = "^0.0.16"
autoflake = {version = "^2.3.0", optional = true}
-python-multipart = "^0.0.9"
+python-multipart = "^0.0.19"
sqlalchemy-utils = "^0.41.2"
pytest-order = {version = "^1.2.0", optional = true}
pytest-asyncio = {version = "^0.23.2", optional = true}
@@ -56,7 +57,7 @@ httpx-sse = "^0.4.0"
isort = { version = "^5.13.2", optional = true }
docker = {version = "^7.1.0", optional = true}
nltk = "^3.8.1"
-jinja2 = "^3.1.4"
+jinja2 = "^3.1.5"
locust = {version = "^2.31.5", optional = true}
wikipedia = {version = "^1.4.0", optional = true}
composio-langchain = "^0.6.15"
@@ -79,6 +80,7 @@ e2b-code-interpreter = {version = "^1.0.3", optional = true}
anthropic = "^0.43.0"
letta_client = "^0.1.16"
+
[tool.poetry.extras]
postgres = ["pgvector", "pg8000", "psycopg2-binary", "psycopg2"]
dev = ["pytest", "pytest-asyncio", "pexpect", "black", "pre-commit", "datasets", "pyright", "pytest-order", "autoflake", "isort", "locust"]
diff --git a/tests/integration_test_tool_execution_sandbox.py b/tests/integration_test_tool_execution_sandbox.py
index aa4cec2c..8a6e5d9d 100644
--- a/tests/integration_test_tool_execution_sandbox.py
+++ b/tests/integration_test_tool_execution_sandbox.py
@@ -195,6 +195,14 @@ def composio_gmail_get_profile_tool(test_user):
yield tool
+@pytest.fixture
+def composio_gmail_get_profile_tool(test_user):
+ tool_manager = ToolManager()
+ tool_create = ToolCreate.from_composio(action_name="GMAIL_GET_PROFILE")
+ tool = tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=test_user)
+ yield tool
+
+
@pytest.fixture
def clear_core_memory_tool(test_user):
def clear_memory(agent_state: "AgentState"):
@@ -418,6 +426,14 @@ def test_local_sandbox_e2e_composio_star_github_without_setting_db_env_vars(
assert result.func_return["details"] == "Action executed successfully"
+@pytest.mark.local_sandbox
+def test_local_sandbox_e2e_composio_star_github_without_setting_db_env_vars(
+ mock_e2b_api_key_none, check_composio_key_set, composio_github_star_tool, test_user
+):
+ result = ToolExecutionSandbox(composio_github_star_tool.name, {"owner": "letta-ai", "repo": "letta"}, user=test_user).run()
+ assert result.func_return["details"] == "Action executed successfully"
+
+
@pytest.mark.local_sandbox
def test_local_sandbox_external_codebase(mock_e2b_api_key_none, custom_test_sandbox_config, external_codebase_tool, test_user):
# Set the args
diff --git a/tests/test_tool_schema_parsing.py b/tests/test_tool_schema_parsing.py
index 27275758..627302ed 100644
--- a/tests/test_tool_schema_parsing.py
+++ b/tests/test_tool_schema_parsing.py
@@ -203,4 +203,5 @@ def test_composio_tool_schema_generation(openai_model: str, structured_output: b
print(f"Successfully called OpenAI using schema {schema} generated from {action_name}\n\n")
except:
print(f"Failed to call OpenAI using schema {schema} generated from {action_name}\n\n")
+
raise