diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 189489614a..620b793fc7 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 0000000000..391047b44b --- /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 0d8f16f7a2..c7b7d3a522 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 9ccb2a505e..a46cddc95d 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 dd79db9586..33e2b67351 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 7bfae1af18..35b0c6f64c 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 @@ def __init__( 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 0000000000..0a355d6970 --- /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 d02d87d183..dcc3ca716f 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 2904a3b630..a509287c02 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 730edd9eab..b952b84c88 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 aa4cec2c1f..8a6e5d9d07 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 272757584e..627302ed59 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