Skip to content

Commit cd11132

Browse files
authored
chore: 0.9.0 release (#2728)
2 parents 2d00ac4 + d32d8a0 commit cd11132

File tree

111 files changed

+7410
-1578
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+7410
-1578
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Add file controls to agent state
2+
3+
Revision ID: c4eb5a907b38
4+
Revises: cce9a6174366
5+
Create Date: 2025-07-21 15:56:57.413000
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
13+
from alembic import op
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "c4eb5a907b38"
17+
down_revision: Union[str, None] = "cce9a6174366"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.add_column("agents", sa.Column("max_files_open", sa.Integer(), nullable=True))
25+
op.add_column("agents", sa.Column("per_file_view_window_char_limit", sa.Integer(), nullable=True))
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade() -> None:
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_column("agents", "per_file_view_window_char_limit")
32+
op.drop_column("agents", "max_files_open")
33+
# ### end Alembic commands ###
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""add prompts
2+
3+
Revision ID: ddecfe4902bc
4+
Revises: c4eb5a907b38
5+
Create Date: 2025-07-21 15:58:13.357459
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
13+
from alembic import op
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "ddecfe4902bc"
17+
down_revision: Union[str, None] = "c4eb5a907b38"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table(
25+
"prompts",
26+
sa.Column("id", sa.String(), nullable=False),
27+
sa.Column("prompt", sa.String(), nullable=False),
28+
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True),
29+
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True),
30+
sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False),
31+
sa.Column("_created_by_id", sa.String(), nullable=True),
32+
sa.Column("_last_updated_by_id", sa.String(), nullable=True),
33+
sa.Column("project_id", sa.String(), nullable=True),
34+
sa.PrimaryKeyConstraint("id"),
35+
)
36+
# ### end Alembic commands ###
37+
38+
39+
def downgrade() -> None:
40+
# ### commands auto generated by Alembic - please adjust! ###
41+
op.drop_table("prompts")
42+
# ### end Alembic commands ###

letta/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
__version__ = version("letta")
66
except PackageNotFoundError:
77
# Fallback for development installations
8-
__version__ = "0.8.17"
8+
__version__ = "0.9.0"
99

1010
if os.environ.get("LETTA_VERSION"):
1111
__version__ = os.environ["LETTA_VERSION"]
1212

13-
1413
# import clients
1514
from letta.client.client import RESTClient
1615

17-
# imports for easier access
16+
# Import sqlite_functions early to ensure event handlers are registered
17+
from letta.orm import sqlite_functions
18+
19+
# # imports for easier access
1820
from letta.schemas.agent import AgentState
1921
from letta.schemas.block import Block
2022
from letta.schemas.embedding_config import EmbeddingConfig

letta/agent.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from letta.llm_api.helpers import calculate_summarizer_cutoff, get_token_counts_for_messages, is_context_overflow_error
3737
from letta.llm_api.llm_api_tools import create
3838
from letta.llm_api.llm_client import LLMClient
39+
from letta.local_llm.constants import INNER_THOUGHTS_KWARG
3940
from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages
4041
from letta.log import get_logger
4142
from letta.memory import summarize_messages
@@ -548,8 +549,8 @@ def _handle_ai_response(
548549
return messages, False, True # force a heartbeat to allow agent to handle error
549550

550551
# Check if inner thoughts is in the function call arguments (possible apparently if you are using Azure)
551-
if "inner_thoughts" in function_args:
552-
response_message.content = function_args.pop("inner_thoughts")
552+
if INNER_THOUGHTS_KWARG in function_args:
553+
response_message.content = function_args.pop(INNER_THOUGHTS_KWARG)
553554
# The content if then internal monologue, not chat
554555
if response_message.content and not nonnull_content:
555556
self.interface.internal_monologue(response_message.content, msg_obj=messages[-1], chunk_index=chunk_index)

letta/agents/base_agent.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ def extract_dynamic_section(text):
122122
curr_dynamic_section = extract_dynamic_section(curr_system_message_text)
123123

124124
# generate just the memory string with current state for comparison
125-
curr_memory_str = agent_state.memory.compile(tool_usage_rules=tool_constraint_block, sources=agent_state.sources)
125+
curr_memory_str = agent_state.memory.compile(
126+
tool_usage_rules=tool_constraint_block, sources=agent_state.sources, max_files_open=agent_state.max_files_open
127+
)
126128
new_dynamic_section = extract_dynamic_section(curr_memory_str)
127129

128130
# compare just the dynamic sections (memory blocks, tool rules, directories)
@@ -149,6 +151,7 @@ def extract_dynamic_section(text):
149151
archival_memory_size=num_archival_memories,
150152
tool_rules_solver=tool_rules_solver,
151153
sources=agent_state.sources,
154+
max_files_open=agent_state.max_files_open,
152155
)
153156

154157
diff = united_diff(curr_system_message_text, new_system_message_str)

letta/agents/voice_agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ async def step_stream(self, input_messages: List[MessageCreate], max_steps: int
153153
previous_message_count=self.num_messages,
154154
archival_memory_size=self.num_archival_memories,
155155
sources=agent_state.sources,
156+
max_files_open=agent_state.max_files_open,
156157
)
157158
letta_message_db_queue = create_input_messages(
158159
input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, actor=self.actor

letta/constants.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_li
326326
CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 5000
327327
CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 5000
328328
CORE_MEMORY_BLOCK_CHAR_LIMIT: int = 5000
329-
CORE_MEMORY_SOURCE_CHAR_LIMIT: int = 50000
329+
330330
# Function return limits
331331
FUNCTION_RETURN_CHAR_LIMIT = 6000 # ~300 words
332332
BASE_FUNCTION_RETURN_CHAR_LIMIT = 1000000 # very high (we rely on implementation)
@@ -361,7 +361,9 @@ def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_li
361361
REDIS_RUN_ID_PREFIX = "agent:send_message:run_id"
362362

363363
# TODO: This is temporary, eventually use token-based eviction
364-
MAX_FILES_OPEN = 5
364+
# File based controls
365+
DEFAULT_MAX_FILES_OPEN = 5
366+
DEFAULT_CORE_MEMORY_SOURCE_CHAR_LIMIT: int = 50000
365367

366368
GET_PROVIDERS_TIMEOUT_SECONDS = 10
367369

letta/functions/schema_generator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,13 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
412412
# Validate that the function has a Google Python style docstring
413413
try:
414414
validate_google_style_docstring(function)
415-
except ValueError:
415+
except ValueError as e:
416416
logger.warning(
417417
f"Function `{function.__name__}` in module `{function.__module__}` "
418418
f"{'(tool_id=' + tool_id + ') ' if tool_id else ''}"
419419
f"is not in Google style docstring format. "
420420
f"Docstring received:\n{repr(function.__doc__[:200]) if function.__doc__ else 'None'}"
421+
f"\nError: {str(e)}"
421422
)
422423

423424
# Get the signature of the function

letta/groups/dynamic_multi_agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def step(
9494
for name, agent_id in [(agents[agent_id].agent_state.name, agent_id) for agent_id in agent_id_options]:
9595
if name.lower() in assistant_message.content.lower():
9696
speaker_id = agent_id
97+
assert speaker_id is not None, f"No names found in {assistant_message.content}"
9798

9899
# Sum usage
99100
total_usage.prompt_tokens += usage_stats.prompt_tokens

letta/helpers/converters.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import base64
21
from typing import Any, Dict, List, Optional, Union
32

43
import numpy as np
@@ -43,7 +42,10 @@
4342
TerminalToolRule,
4443
ToolRule,
4544
)
45+
from letta.settings import DatabaseChoice, settings
4646

47+
if settings.database_engine == DatabaseChoice.SQLITE:
48+
import sqlite_vec
4749
# --------------------------
4850
# LLMConfig Serialization
4951
# --------------------------
@@ -272,22 +274,28 @@ def deserialize_message_content(data: Optional[List[Dict]]) -> List[MessageConte
272274

273275

274276
def serialize_vector(vector: Optional[Union[List[float], np.ndarray]]) -> Optional[bytes]:
275-
"""Convert a NumPy array or list into a base64-encoded byte string."""
277+
"""Convert a NumPy array or list into serialized format using sqlite-vec."""
276278
if vector is None:
277279
return None
278280
if isinstance(vector, list):
279281
vector = np.array(vector, dtype=np.float32)
282+
else:
283+
vector = vector.astype(np.float32)
280284

281-
return base64.b64encode(vector.tobytes())
285+
return sqlite_vec.serialize_float32(vector.tolist())
282286

283287

284288
def deserialize_vector(data: Optional[bytes], dialect: Dialect) -> Optional[np.ndarray]:
285-
"""Convert a base64-encoded byte string back into a NumPy array."""
289+
"""Convert serialized data back into a NumPy array using sqlite-vec format."""
286290
if not data:
287291
return None
288292

289293
if dialect.name == "sqlite":
290-
data = base64.b64decode(data)
294+
# Use sqlite-vec format
295+
if len(data) % 4 == 0: # Must be divisible by 4 for float32
296+
return np.frombuffer(data, dtype=np.float32)
297+
else:
298+
raise ValueError(f"Invalid sqlite-vec binary data length: {len(data)}")
291299

292300
return np.frombuffer(data, dtype=np.float32)
293301

0 commit comments

Comments
 (0)