From f5c4ab50f4cd1d6092b2745af20107db21554e9e Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:49:22 -0800 Subject: [PATCH] chore: add ty + pre-commit hook and repeal even more ruff rules (#9504) * auto fixes * auto fix pt2 and transitive deps and undefined var checking locals() * manual fixes (ignored or letta-code fixed) * fix circular import * remove all ignores, add FastAPI rules and Ruff rules * add ty and precommit * ruff stuff * ty check fixes * ty check fixes pt 2 * error on invalid --- .../generate_model_sweep_markdown.py | 6 +- .github/scripts/model-sweep/model_sweep.py | 2 +- .pre-commit-config.yaml | 7 ++ letta/adapters/letta_llm_stream_adapter.py | 2 +- letta/adapters/sglang_native_adapter.py | 2 +- letta/agents/base_agent.py | 2 +- letta/agents/ephemeral_summary_agent.py | 2 +- letta/agents/helpers.py | 7 +- letta/agents/letta_agent.py | 6 +- letta/agents/letta_agent_batch.py | 2 +- letta/agents/letta_agent_v2.py | 10 +-- letta/agents/letta_agent_v3.py | 9 ++- letta/agents/voice_agent.py | 10 +-- letta/agents/voice_sleeptime_agent.py | 13 ++-- letta/data_sources/connectors.py | 15 +++- letta/data_sources/redis_client.py | 4 +- letta/errors.py | 7 +- letta/functions/function_sets/base.py | 15 ++-- letta/functions/function_sets/multi_agent.py | 13 ++-- letta/functions/function_sets/voice.py | 13 ++-- letta/functions/functions.py | 4 +- letta/functions/helpers.py | 54 +++++++++------ letta/functions/schema_generator.py | 4 +- letta/functions/schema_validator.py | 2 +- letta/groups/dynamic_multi_agent.py | 9 ++- letta/groups/helpers.py | 2 +- letta/groups/sleeptime_multi_agent_v2.py | 2 +- letta/helpers/converters.py | 9 ++- letta/helpers/pinecone_utils.py | 4 +- letta/helpers/tool_execution_helper.py | 2 +- letta/helpers/tpuf_client.py | 42 +++++------ letta/interface.py | 2 +- ..._parallel_tool_call_streaming_interface.py | 13 ++-- .../anthropic_streaming_interface.py | 23 ++++--- .../interfaces/gemini_streaming_interface.py | 13 ++-- .../interfaces/openai_streaming_interface.py | 41 ++++++----- letta/jobs/scheduler.py | 2 +- letta/llm_api/anthropic_client.py | 14 ++-- letta/llm_api/google_vertex_client.py | 6 +- letta/llm_api/openai.py | 2 +- letta/llm_api/openai_client.py | 6 +- letta/local_llm/chat_completion_proxy.py | 10 +-- .../grammars/gbnf_grammar_generator.py | 50 +++++++------- letta/local_llm/koboldcpp/api.py | 5 +- letta/local_llm/llamacpp/api.py | 5 +- .../llm_chat_completion_wrappers/airoboros.py | 8 +-- .../llm_chat_completion_wrappers/chatml.py | 12 ++-- .../configurable_wrapper.py | 10 +-- .../llm_chat_completion_wrappers/dolphin.py | 4 +- .../llm_chat_completion_wrappers/llama3.py | 12 ++-- .../simple_summary_wrapper.py | 6 +- .../llm_chat_completion_wrappers/zephyr.py | 6 +- letta/local_llm/lmstudio/api.py | 4 +- letta/local_llm/ollama/api.py | 4 +- letta/local_llm/vllm/api.py | 5 +- letta/local_llm/webui/api.py | 5 +- letta/local_llm/webui/legacy_api.py | 5 +- letta/orm/agent.py | 11 +-- letta/orm/agents_tags.py | 7 +- letta/orm/archives_agents.py | 9 ++- letta/orm/base.py | 2 +- letta/orm/block.py | 10 +-- letta/orm/blocks_tags.py | 7 +- letta/orm/files_agents.py | 4 +- letta/orm/group.py | 15 ++-- letta/orm/identity.py | 13 ++-- letta/orm/llm_batch_items.py | 13 ++-- letta/orm/llm_batch_job.py | 10 ++- letta/orm/message.py | 16 +++-- letta/orm/passage.py | 3 +- letta/orm/provider_trace.py | 7 +- letta/orm/provider_trace_metadata.py | 7 +- letta/orm/sqlalchemy_base.py | 35 +++++----- letta/otel/tracing.py | 2 +- letta/schemas/letta_message.py | 4 +- letta/schemas/message.py | 12 ++-- letta/schemas/providers/__init__.py | 20 +++--- letta/schemas/providers/lmstudio.py | 2 +- letta/schemas/providers/openrouter.py | 2 +- letta/schemas/tool.py | 2 +- letta/schemas/usage.py | 4 +- letta/serialize_schemas/marshmallow_agent.py | 7 +- .../marshmallow_agent_environment_variable.py | 2 +- letta/serialize_schemas/marshmallow_block.py | 2 +- .../serialize_schemas/marshmallow_message.py | 2 +- letta/serialize_schemas/marshmallow_tag.py | 2 +- letta/serialize_schemas/marshmallow_tool.py | 2 +- letta/server/rest_api/app.py | 2 +- letta/server/rest_api/auth/index.py | 2 +- letta/server/rest_api/interface.py | 4 +- letta/server/rest_api/proxy_helpers.py | 6 +- letta/server/rest_api/redis_stream_manager.py | 4 +- letta/server/rest_api/routers/v1/agents.py | 13 ++-- letta/server/rest_api/routers/v1/anthropic.py | 10 ++- letta/server/rest_api/routers/v1/folders.py | 2 +- letta/server/rest_api/routers/v1/git_http.py | 8 ++- .../server/rest_api/routers/v1/identities.py | 2 +- letta/server/rest_api/routers/v1/sources.py | 2 +- letta/server/rest_api/routers/v1/steps.py | 2 +- letta/server/rest_api/routers/v1/telemetry.py | 2 +- letta/server/rest_api/routers/v1/zai.py | 10 ++- letta/server/rest_api/streaming_response.py | 2 +- letta/server/rest_api/utils.py | 4 +- letta/server/server.py | 4 +- letta/server/ws_api/server.py | 2 +- letta/services/agent_manager.py | 2 +- letta/services/agent_serialization_manager.py | 4 +- .../context_window_calculator.py | 2 +- letta/services/file_manager.py | 6 +- .../chunker/llama_index_chunker.py | 4 +- .../embedder/openai_embedder.py | 2 +- letta/services/files_agents_manager.py | 2 +- letta/services/group_manager.py | 2 +- .../services/helpers/agent_manager_helper.py | 56 ++++++--------- letta/services/llm_trace_writer.py | 8 ++- letta/services/mcp/oauth_utils.py | 17 +++-- letta/services/mcp_manager.py | 4 +- letta/services/mcp_server_manager.py | 2 +- letta/services/memory_repo/__init__.py | 6 +- letta/services/memory_repo/git_operations.py | 2 +- .../services/memory_repo/memfs_client_base.py | 4 +- .../services/memory_repo/storage/__init__.py | 2 +- letta/services/passage_manager.py | 15 ++-- letta/services/provider_manager.py | 4 +- letta/services/run_manager.py | 2 +- letta/services/streaming_service.py | 3 +- letta/services/summarizer/summarizer.py | 14 ++-- letta/services/summarizer/summarizer_all.py | 2 +- .../summarizer/summarizer_sliding_window.py | 13 ++-- .../tool_executor/core_tool_executor.py | 6 +- .../tool_executor/files_tool_executor.py | 8 +-- .../tool_executor/tool_execution_manager.py | 4 +- .../tool_executor/tool_execution_sandbox.py | 13 ++-- letta/services/tool_manager.py | 6 +- letta/services/tool_sandbox/base.py | 4 +- .../services/tool_sandbox/modal_sandbox_v2.py | 2 +- letta/services/tool_sandbox/safe_pickle.py | 2 +- letta/streaming_interface.py | 2 +- letta/system.py | 6 +- letta/utils.py | 2 +- pyproject.toml | 49 +++++++++---- sandbox/modal_executor.py | 4 +- tests/helpers/client_helper.py | 2 +- tests/helpers/endpoints_helper.py | 2 +- tests/integration_test_builtin_tools.py | 4 +- tests/integration_test_client_side_tools.py | 2 +- tests/integration_test_human_in_the_loop.py | 6 +- tests/integration_test_multi_agent.py | 6 +- tests/integration_test_send_message.py | 14 ++-- tests/integration_test_send_message_v2.py | 12 ++-- tests/integration_test_summarizer.py | 20 +++--- tests/integration_test_turbopuffer.py | 52 ++++++-------- tests/integration_test_usage_tracking.py | 2 +- tests/managers/conftest.py | 2 +- tests/managers/test_agent_manager.py | 34 ++++----- tests/managers/test_cancellation.py | 12 ++-- tests/managers/test_file_manager.py | 24 +++---- tests/managers/test_identity_manager.py | 28 -------- tests/managers/test_mcp_manager.py | 4 +- tests/managers/test_message_manager.py | 8 +-- tests/managers/test_provider_manager.py | 2 +- tests/managers/test_run_manager.py | 19 +++-- tests/managers/test_source_manager.py | 28 ++++---- tests/managers/test_tool_manager.py | 4 +- tests/manual_test_many_messages.py | 2 +- tests/mcp_tests/test_schema_validator.py | 2 +- .../test_insert_archival_memory.py | 2 +- tests/sdk/mcp_servers_test.py | 8 +-- tests/sdk/search_test.py | 12 ++-- tests/test_agent_serialization.py | 2 +- tests/test_agent_serialization_v2.py | 4 +- tests/test_client.py | 9 ++- tests/test_google_embeddings.py | 3 +- tests/test_internal_agents_count.py | 2 +- tests/test_letta_agent_batch.py | 3 +- tests/test_llm_clients.py | 2 +- tests/test_minimax_client.py | 17 +++-- tests/test_prompt_caching.py | 2 +- tests/test_redis_client.py | 2 +- tests/test_sdk_client.py | 20 ++---- tests/test_server.py | 4 +- tests/test_server_providers.py | 9 ++- ...st_sonnet_nonnative_reasoning_buffering.py | 2 +- tests/test_sources.py | 14 ++-- tests/test_tool_rule_solver.py | 6 +- tests/test_tool_schema_parsing.py | 2 +- tests/test_utils.py | 12 ++-- uv.lock | 69 +++++++++++++------ 188 files changed, 906 insertions(+), 746 deletions(-) diff --git a/.github/scripts/model-sweep/generate_model_sweep_markdown.py b/.github/scripts/model-sweep/generate_model_sweep_markdown.py index 38552a8c..c82e051d 100644 --- a/.github/scripts/model-sweep/generate_model_sweep_markdown.py +++ b/.github/scripts/model-sweep/generate_model_sweep_markdown.py @@ -31,7 +31,7 @@ def get_support_status(passed_tests, feature_tests): # Filter out error tests when checking for support non_error_tests = [test for test in feature_tests if not test.endswith("_error")] - error_tests = [test for test in feature_tests if test.endswith("_error")] + [test for test in feature_tests if test.endswith("_error")] # Check which non-error tests passed passed_non_error_tests = [test for test in non_error_tests if test in passed_tests] @@ -137,7 +137,7 @@ def get_github_repo_info(): else: return None return repo_path - except: + except Exception: pass # Default fallback @@ -335,7 +335,7 @@ def process_model_sweep_report(input_file, output_file, config_file=None, debug= # Format timestamp if it's a full ISO string if "T" in str(last_scanned): last_scanned = str(last_scanned).split("T")[0] # Just the date part - except: + except Exception: last_scanned = "Unknown" # Calculate support score for ranking diff --git a/.github/scripts/model-sweep/model_sweep.py b/.github/scripts/model-sweep/model_sweep.py index ec61e936..086ea0a4 100644 --- a/.github/scripts/model-sweep/model_sweep.py +++ b/.github/scripts/model-sweep/model_sweep.py @@ -690,7 +690,7 @@ def test_token_streaming_agent_loop_error( stream_tokens=True, ) list(response) - except: + except Exception: pass # only some models throw an error TODO: make this consistent messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dade61ca..90fd016c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,3 +23,10 @@ repos: - id: ruff-check args: [ --fix ] - id: ruff-format + + - repo: local + hooks: + - id: ty + name: ty check + entry: uv run ty check . + language: python diff --git a/letta/adapters/letta_llm_stream_adapter.py b/letta/adapters/letta_llm_stream_adapter.py index de1e47e4..76fc6d65 100644 --- a/letta/adapters/letta_llm_stream_adapter.py +++ b/letta/adapters/letta_llm_stream_adapter.py @@ -143,7 +143,7 @@ class LettaLLMStreamAdapter(LettaLLMAdapter): # Extract tool call from the interface try: self.tool_call = self.interface.get_tool_call_object() - except ValueError as e: + except ValueError: # No tool call, handle upstream self.tool_call = None diff --git a/letta/adapters/sglang_native_adapter.py b/letta/adapters/sglang_native_adapter.py index ad0f0e88..cab8b267 100644 --- a/letta/adapters/sglang_native_adapter.py +++ b/letta/adapters/sglang_native_adapter.py @@ -292,7 +292,7 @@ class SGLangNativeAdapter(SimpleLLMRequestAdapter): if isinstance(tc_args, str): try: tc_args = json.loads(tc_args) - except: + except Exception: pass tc_parts.append(f'\n{{"name": "{tc_name}", "arguments": {json.dumps(tc_args)}}}\n') diff --git a/letta/agents/base_agent.py b/letta/agents/base_agent.py index ddc5309e..326dc60a 100644 --- a/letta/agents/base_agent.py +++ b/letta/agents/base_agent.py @@ -168,7 +168,7 @@ class BaseAgent(ABC): actor=self.actor, project_id=agent_state.project_id, ) - return [new_system_message] + in_context_messages[1:] + return [new_system_message, *in_context_messages[1:]] else: return in_context_messages diff --git a/letta/agents/ephemeral_summary_agent.py b/letta/agents/ephemeral_summary_agent.py index ca73d800..86b2b90a 100644 --- a/letta/agents/ephemeral_summary_agent.py +++ b/letta/agents/ephemeral_summary_agent.py @@ -79,7 +79,7 @@ class EphemeralSummaryAgent(BaseAgent): content=[TextContent(text=get_system_text("summary_system_prompt"))], ) messages = await convert_message_creates_to_messages( - message_creates=[system_message_create] + input_messages, + message_creates=[system_message_create, *input_messages], agent_id=self.agent_id, timezone=agent_state.timezone, run_id=None, # TODO: add this diff --git a/letta/agents/helpers.py b/letta/agents/helpers.py index 7f53aa69..70fdd856 100644 --- a/letta/agents/helpers.py +++ b/letta/agents/helpers.py @@ -1,8 +1,11 @@ import json import xml.etree.ElementTree as ET -from typing import Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple from uuid import UUID, uuid4 +if TYPE_CHECKING: + from letta.schemas.tool import Tool + from letta.errors import LettaError, PendingApprovalError from letta.helpers import ToolRulesSolver from letta.helpers.datetime_helpers import get_utc_time @@ -462,7 +465,7 @@ def _schema_accepts_value(prop_schema: Dict[str, Any], value: Any) -> bool: return True -def merge_and_validate_prefilled_args(tool: "Tool", llm_args: Dict[str, Any], prefilled_args: Dict[str, Any]) -> Dict[str, Any]: # noqa: F821 +def merge_and_validate_prefilled_args(tool: "Tool", llm_args: Dict[str, Any], prefilled_args: Dict[str, Any]) -> Dict[str, Any]: """Merge LLM-provided args with prefilled args from tool rules. - Overlapping keys are replaced by prefilled values (prefilled wins). diff --git a/letta/agents/letta_agent.py b/letta/agents/letta_agent.py index a9fa5dea..be6a378b 100644 --- a/letta/agents/letta_agent.py +++ b/letta/agents/letta_agent.py @@ -1574,7 +1574,7 @@ class LettaAgent(BaseAgent): self.logger.warning( f"Total tokens {total_tokens} exceeds configured max tokens {llm_config.context_window}, forcefully clearing message history." ) - new_in_context_messages, updated = await self.summarizer.summarize( + new_in_context_messages, _updated = await self.summarizer.summarize( in_context_messages=in_context_messages, new_letta_messages=new_letta_messages, force=True, @@ -1587,7 +1587,7 @@ class LettaAgent(BaseAgent): self.logger.info( f"Total tokens {total_tokens} does not exceed configured max tokens {llm_config.context_window}, passing summarizing w/o force." ) - new_in_context_messages, updated = await self.summarizer.summarize( + new_in_context_messages, _updated = await self.summarizer.summarize( in_context_messages=in_context_messages, new_letta_messages=new_letta_messages, run_id=run_id, @@ -1607,7 +1607,7 @@ class LettaAgent(BaseAgent): agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=self.agent_id, actor=self.actor) message_ids = agent_state.message_ids in_context_messages = await self.message_manager.get_messages_by_ids_async(message_ids=message_ids, actor=self.actor) - new_in_context_messages, updated = await self.summarizer.summarize( + new_in_context_messages, _updated = await self.summarizer.summarize( in_context_messages=in_context_messages, new_letta_messages=[], force=True ) return await self.agent_manager.update_message_ids_async( diff --git a/letta/agents/letta_agent_batch.py b/letta/agents/letta_agent_batch.py index 7bcc74f0..35d8c8f6 100644 --- a/letta/agents/letta_agent_batch.py +++ b/letta/agents/letta_agent_batch.py @@ -217,7 +217,7 @@ class LettaAgentBatch(BaseAgent): if batch_items: log_event(name="bulk_create_batch_items") - batch_items_persisted = await self.batch_manager.create_llm_batch_items_bulk_async(batch_items, actor=self.actor) + await self.batch_manager.create_llm_batch_items_bulk_async(batch_items, actor=self.actor) log_event(name="return_batch_response") return LettaBatchResponse( diff --git a/letta/agents/letta_agent_v2.py b/letta/agents/letta_agent_v2.py index ba41c123..686d49fb 100644 --- a/letta/agents/letta_agent_v2.py +++ b/letta/agents/letta_agent_v2.py @@ -456,7 +456,7 @@ class LettaAgentV2(BaseAgentV2): step_progression = StepProgression.START caught_exception = None # TODO(@caren): clean this up - tool_call, reasoning_content, agent_step_span, first_chunk, step_id, logged_step, step_start_ns, step_metrics = ( + tool_call, reasoning_content, agent_step_span, first_chunk, step_id, logged_step, _step_start_ns, step_metrics = ( None, None, None, @@ -752,7 +752,7 @@ class LettaAgentV2(BaseAgentV2): num_archival_memories=None, force=True, ) - except Exception as e: + except Exception: raise # Always scrub inner thoughts regardless of system prompt refresh @@ -835,7 +835,7 @@ class LettaAgentV2(BaseAgentV2): new_system_message = await self.message_manager.update_message_by_id_async( curr_system_message.id, message_update=MessageUpdate(content=new_system_message_str), actor=self.actor ) - return [new_system_message] + in_context_messages[1:] + return [new_system_message, *in_context_messages[1:]] else: return in_context_messages @@ -1322,7 +1322,7 @@ class LettaAgentV2(BaseAgentV2): self.logger.warning( f"Total tokens {total_tokens} exceeds configured max tokens {self.agent_state.llm_config.context_window}, forcefully clearing message history." ) - new_in_context_messages, updated = await self.summarizer.summarize( + new_in_context_messages, _updated = await self.summarizer.summarize( in_context_messages=in_context_messages, new_letta_messages=new_letta_messages, force=True, @@ -1335,7 +1335,7 @@ class LettaAgentV2(BaseAgentV2): self.logger.info( f"Total tokens {total_tokens} does not exceed configured max tokens {self.agent_state.llm_config.context_window}, passing summarizing w/o force." ) - new_in_context_messages, updated = await self.summarizer.summarize( + new_in_context_messages, _updated = await self.summarizer.summarize( in_context_messages=in_context_messages, new_letta_messages=new_letta_messages, run_id=run_id, diff --git a/letta/agents/letta_agent_v3.py b/letta/agents/letta_agent_v3.py index 8665f4e1..4befe589 100644 --- a/letta/agents/letta_agent_v3.py +++ b/letta/agents/letta_agent_v3.py @@ -644,7 +644,7 @@ class LettaAgentV3(LettaAgentV2): message.conversation_id = self.conversation_id # persist the new message objects - ONLY place where messages are persisted - persisted_messages = await self.message_manager.create_many_messages_async( + await self.message_manager.create_many_messages_async( new_messages, actor=self.actor, run_id=run_id, @@ -799,7 +799,7 @@ class LettaAgentV3(LettaAgentV2): step_progression = StepProgression.START caught_exception = None # TODO(@caren): clean this up - tool_calls, content, agent_step_span, first_chunk, step_id, logged_step, step_start_ns, step_metrics = ( + tool_calls, content, agent_step_span, _first_chunk, step_id, logged_step, _step_start_ns, step_metrics = ( None, None, None, @@ -971,7 +971,6 @@ class LettaAgentV3(LettaAgentV2): async for chunk in invocation: if llm_adapter.supports_token_streaming(): if include_return_message_types is None or chunk.message_type in include_return_message_types: - first_chunk = True yield chunk # If you've reached this point without an error, break out of retry loop break @@ -1659,10 +1658,10 @@ class LettaAgentV3(LettaAgentV2): # Decide continuation for this tool if has_prefill_error: cont = False - hb_reason = None + _hb_reason = None sr = LettaStopReason(stop_reason=StopReasonType.invalid_tool_call.value) else: - cont, hb_reason, sr = self._decide_continuation( + cont, _hb_reason, sr = self._decide_continuation( agent_state=self.agent_state, tool_call_name=spec["name"], tool_rule_violated=spec["violated"], diff --git a/letta/agents/voice_agent.py b/letta/agents/voice_agent.py index 4f26aaa7..1bf41729 100644 --- a/letta/agents/voice_agent.py +++ b/letta/agents/voice_agent.py @@ -1,10 +1,13 @@ import json import uuid from datetime import datetime, timedelta, timezone -from typing import Any, AsyncGenerator, Dict, List, Optional +from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional import openai +if TYPE_CHECKING: + from letta.schemas.tool_execution_result import ToolExecutionResult + from letta.agents.base_agent import BaseAgent from letta.agents.exceptions import IncompatibleAgentType from letta.agents.voice_sleeptime_agent import VoiceSleeptimeAgent @@ -250,7 +253,6 @@ class VoiceAgent(BaseAgent): agent_state=agent_state, ) tool_result = tool_execution_result.func_return - success_flag = tool_execution_result.success_flag # 3. Provide function_call response back into the conversation # TODO: fix this tool format @@ -292,7 +294,7 @@ class VoiceAgent(BaseAgent): new_letta_messages = await self.message_manager.create_many_messages_async(letta_message_db_queue, actor=self.actor) # TODO: Make this more general and configurable, less brittle - new_in_context_messages, updated = await summarizer.summarize( + new_in_context_messages, _updated = await summarizer.summarize( in_context_messages=in_context_messages, new_letta_messages=new_letta_messages ) @@ -414,7 +416,7 @@ class VoiceAgent(BaseAgent): for t in tools ] - async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) -> "ToolExecutionResult": # noqa: F821 + async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) -> "ToolExecutionResult": """ Executes a tool and returns the ToolExecutionResult. """ diff --git a/letta/agents/voice_sleeptime_agent.py b/letta/agents/voice_sleeptime_agent.py index 264b4204..6f7f184f 100644 --- a/letta/agents/voice_sleeptime_agent.py +++ b/letta/agents/voice_sleeptime_agent.py @@ -1,4 +1,9 @@ -from typing import AsyncGenerator, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, AsyncGenerator, List, Optional, Tuple, Union + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + from letta.schemas.tool_execution_result import ToolExecutionResult from letta.agents.helpers import _create_letta_response, serialize_message_history from letta.agents.letta_agent import LettaAgent @@ -89,7 +94,7 @@ class VoiceSleeptimeAgent(LettaAgent): current_in_context_messages, new_in_context_messages, stop_reason, usage = await super()._step( agent_state=agent_state, input_messages=input_messages, max_steps=max_steps ) - new_in_context_messages, updated = await self.summarizer.summarize( + new_in_context_messages, _updated = await self.summarizer.summarize( in_context_messages=current_in_context_messages, new_letta_messages=new_in_context_messages ) self.agent_manager.set_in_context_messages( @@ -110,9 +115,9 @@ class VoiceSleeptimeAgent(LettaAgent): tool_name: str, tool_args: JsonDict, agent_state: AgentState, - agent_step_span: Optional["Span"] = None, # noqa: F821 + agent_step_span: Optional["Span"] = None, step_id: str | None = None, - ) -> "ToolExecutionResult": # noqa: F821 + ) -> "ToolExecutionResult": """ Executes a tool and returns the ToolExecutionResult """ diff --git a/letta/data_sources/connectors.py b/letta/data_sources/connectors.py index 52a7b73f..1cfd2b21 100644 --- a/letta/data_sources/connectors.py +++ b/letta/data_sources/connectors.py @@ -1,4 +1,7 @@ -from typing import Dict, Iterator, List, Tuple +from typing import TYPE_CHECKING, Dict, Iterator, List, Tuple + +if TYPE_CHECKING: + from letta.schemas.user import User import typer @@ -37,7 +40,7 @@ class DataConnector: """ -async def load_data(connector: DataConnector, source: Source, passage_manager: PassageManager, file_manager: FileManager, actor: "User"): # noqa: F821 +async def load_data(connector: DataConnector, source: Source, passage_manager: PassageManager, file_manager: FileManager, actor: "User"): from letta.llm_api.llm_client import LLMClient """Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id.""" @@ -143,7 +146,13 @@ async def load_data(connector: DataConnector, source: Source, passage_manager: P class DirectoryConnector(DataConnector): - def __init__(self, input_files: List[str] = None, input_directory: str = None, recursive: bool = False, extensions: List[str] = None): + def __init__( + self, + input_files: List[str] | None = None, + input_directory: str | None = None, + recursive: bool = False, + extensions: List[str] | None = None, + ): """ Connector for reading text data from a directory of files. diff --git a/letta/data_sources/redis_client.py b/letta/data_sources/redis_client.py index 5bbb6987..c1f7a098 100644 --- a/letta/data_sources/redis_client.py +++ b/letta/data_sources/redis_client.py @@ -149,7 +149,7 @@ class AsyncRedisClient: try: client = await self.get_client() return await client.get(key) - except: + except Exception: return default @with_retry() @@ -320,7 +320,7 @@ class AsyncRedisClient: client = await self.get_client() result = await client.smismember(key, values) return result if isinstance(values, list) else result[0] - except: + except Exception: return [0] * len(values) if isinstance(values, list) else 0 async def srem(self, key: str, *members: Union[str, int, float]) -> int: diff --git a/letta/errors.py b/letta/errors.py index 2d2dfe05..f725b2b5 100644 --- a/letta/errors.py +++ b/letta/errors.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Union # Avoid circular imports if TYPE_CHECKING: + from letta.schemas.letta_message import LettaMessage from letta.schemas.message import Message @@ -362,16 +363,16 @@ class RateLimitExceededError(LettaError): class LettaMessageError(LettaError): """Base error class for handling message-related errors.""" - messages: List[Union["Message", "LettaMessage"]] # noqa: F821 + messages: List[Union["Message", "LettaMessage"]] default_error_message: str = "An error occurred with the message." - def __init__(self, *, messages: List[Union["Message", "LettaMessage"]], explanation: Optional[str] = None) -> None: # noqa: F821 + def __init__(self, *, messages: List[Union["Message", "LettaMessage"]], explanation: Optional[str] = None) -> None: error_msg = self.construct_error_message(messages, self.default_error_message, explanation) super().__init__(error_msg) self.messages = messages @staticmethod - def construct_error_message(messages: List[Union["Message", "LettaMessage"]], error_msg: str, explanation: Optional[str] = None) -> str: # noqa: F821 + def construct_error_message(messages: List[Union["Message", "LettaMessage"]], error_msg: str, explanation: Optional[str] = None) -> str: """Helper method to construct a clean and formatted error message.""" if explanation: error_msg += f" (Explanation: {explanation})" diff --git a/letta/functions/function_sets/base.py b/letta/functions/function_sets/base.py index 28926f17..4e866d18 100644 --- a/letta/functions/function_sets/base.py +++ b/letta/functions/function_sets/base.py @@ -1,10 +1,11 @@ from typing import TYPE_CHECKING, List, Literal, Optional -from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING - if TYPE_CHECKING: + from letta.agents.letta_agent import LettaAgent as Agent from letta.schemas.agent import AgentState +from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING + def memory( agent_state: "AgentState", @@ -67,7 +68,7 @@ def memory( raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.") -def send_message(self: "Agent", message: str) -> Optional[str]: # noqa: F821 +def send_message(self: "Agent", message: str) -> Optional[str]: """ Sends a message to the human user. @@ -84,7 +85,7 @@ def send_message(self: "Agent", message: str) -> Optional[str]: # noqa: F821 def conversation_search( - self: "Agent", # noqa: F821 + self: "Agent", query: Optional[str] = None, roles: Optional[List[Literal["assistant", "user", "tool"]]] = None, limit: Optional[int] = None, @@ -160,7 +161,7 @@ def conversation_search( return results_str -async def archival_memory_insert(self: "Agent", content: str, tags: Optional[list[str]] = None) -> Optional[str]: # noqa: F821 +async def archival_memory_insert(self: "Agent", content: str, tags: Optional[list[str]] = None) -> Optional[str]: """ Add information to long-term archival memory for later retrieval. @@ -191,7 +192,7 @@ async def archival_memory_insert(self: "Agent", content: str, tags: Optional[lis async def archival_memory_search( - self: "Agent", # noqa: F821 + self: "Agent", query: str, tags: Optional[list[str]] = None, tag_match_mode: Literal["any", "all"] = "any", @@ -431,7 +432,7 @@ def memory_insert(agent_state: "AgentState", label: str, new_str: str, insert_li # Insert the new string as a line new_str_lines = new_str.split("\n") new_value_lines = current_value_lines[:insert_line] + new_str_lines + current_value_lines[insert_line:] - snippet_lines = ( + ( current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line] + new_str_lines + current_value_lines[insert_line : insert_line + SNIPPET_LINES] diff --git a/letta/functions/function_sets/multi_agent.py b/letta/functions/function_sets/multi_agent.py index b1dbda98..43c90109 100644 --- a/letta/functions/function_sets/multi_agent.py +++ b/letta/functions/function_sets/multi_agent.py @@ -1,5 +1,8 @@ import asyncio -from typing import List +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from letta.agents.letta_agent import LettaAgent as Agent from letta.functions.helpers import ( _send_message_to_agents_matching_tags_async, @@ -13,7 +16,7 @@ from letta.server.rest_api.dependencies import get_letta_server from letta.settings import settings -def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_agent_id: str) -> str: # noqa: F821 +def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_agent_id: str) -> str: """ Sends a message to a specific Letta agent within the same organization and waits for a response. The sender's identity is automatically included, so no explicit introduction is needed in the message. This function is designed for two-way communication where a reply is expected. @@ -37,7 +40,7 @@ def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_ ) -def send_message_to_agents_matching_tags(self: "Agent", message: str, match_all: List[str], match_some: List[str]) -> List[str]: # noqa: F821 +def send_message_to_agents_matching_tags(self: "Agent", message: str, match_all: List[str], match_some: List[str]) -> List[str]: """ Sends a message to all agents within the same organization that match the specified tag criteria. Agents must possess *all* of the tags in `match_all` and *at least one* of the tags in `match_some` to receive the message. @@ -66,7 +69,7 @@ def send_message_to_agents_matching_tags(self: "Agent", message: str, match_all: return asyncio.run(_send_message_to_agents_matching_tags_async(self, server, messages, matching_agents)) -def send_message_to_all_agents_in_group(self: "Agent", message: str) -> List[str]: # noqa: F821 +def send_message_to_all_agents_in_group(self: "Agent", message: str) -> List[str]: """ Sends a message to all agents within the same multi-agent group. @@ -82,7 +85,7 @@ def send_message_to_all_agents_in_group(self: "Agent", message: str) -> List[str return asyncio.run(_send_message_to_all_agents_in_group_async(self, message)) -def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str) -> str: # noqa: F821 +def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str) -> str: """ Sends a message to a specific Letta agent within the same organization. The sender's identity is automatically included, so no explicit introduction is required in the message. This function does not expect a response from the target agent, making it suitable for notifications or one-way communication. Args: diff --git a/letta/functions/function_sets/voice.py b/letta/functions/function_sets/voice.py index ee67965b..c46188a2 100644 --- a/letta/functions/function_sets/voice.py +++ b/letta/functions/function_sets/voice.py @@ -1,10 +1,13 @@ ## Voice chat + sleeptime tools -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from letta.schemas.agent import AgentState from pydantic import BaseModel, Field -def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None: # noqa: F821 +def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None: """ Rewrite memory block for the main agent, new_memory should contain all current information from the block that is not outdated or inconsistent, integrating any new information, resulting in a new memory block that is organized, readable, and comprehensive. @@ -18,7 +21,7 @@ def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None: # return None -def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore # noqa: F821 +def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore """ This function is called when the agent is done rethinking the memory. @@ -43,7 +46,7 @@ class MemoryChunk(BaseModel): ) -def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None: # noqa: F821 +def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None: """ Persist dialogue that is about to fall out of the agent’s context window. @@ -59,7 +62,7 @@ def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None def search_memory( - agent_state: "AgentState", # noqa: F821 + agent_state: "AgentState", convo_keyword_queries: Optional[List[str]], start_minutes_ago: Optional[int], end_minutes_ago: Optional[int], diff --git a/letta/functions/functions.py b/letta/functions/functions.py index c35a48c6..01c7d4e2 100644 --- a/letta/functions/functions.py +++ b/letta/functions/functions.py @@ -179,7 +179,7 @@ def _extract_pydantic_classes(tree: ast.AST, imports_map: Dict[str, Any]) -> Dic pass # Field is required, no default else: field_kwargs["default"] = default_val - except: + except Exception: pass fields[field_name] = Field(**field_kwargs) @@ -188,7 +188,7 @@ def _extract_pydantic_classes(tree: ast.AST, imports_map: Dict[str, Any]) -> Dic try: default_val = ast.literal_eval(stmt.value) fields[field_name] = default_val - except: + except Exception: pass # Create the dynamic Pydantic model diff --git a/letta/functions/helpers.py b/letta/functions/helpers.py index b74cc070..ce023a4b 100644 --- a/letta/functions/helpers.py +++ b/letta/functions/helpers.py @@ -3,7 +3,17 @@ import json import logging import threading from random import uniform -from typing import Any, Dict, List, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union + +if TYPE_CHECKING: + from letta.agents.letta_agent import LettaAgent as Agent + from letta.schemas.agent import AgentState + from letta.server.server import SyncServer + + try: + from langchain.tools.base import BaseTool as LangChainBaseTool + except ImportError: + LangChainBaseTool = None import humps from pydantic import BaseModel, Field, create_model @@ -21,6 +31,8 @@ from letta.server.rest_api.dependencies import get_letta_server from letta.settings import settings from letta.utils import safe_create_task +_background_tasks: set[asyncio.Task] = set() + # TODO needed? def generate_mcp_tool_wrapper(mcp_tool_name: str) -> tuple[str, str]: @@ -36,8 +48,8 @@ def {mcp_tool_name}(**kwargs): def generate_langchain_tool_wrapper( - tool: "LangChainBaseTool", # noqa: F821 - additional_imports_module_attr_map: dict[str, str] = None, + tool: "LangChainBaseTool", + additional_imports_module_attr_map: dict[str, str] | None = None, ) -> tuple[str, str]: tool_name = tool.__class__.__name__ import_statement = f"from langchain_community.tools import {tool_name}" @@ -73,7 +85,7 @@ def _assert_code_gen_compilable(code_str): print(f"Syntax error in code: {e}") -def _assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional_imports_module_attr_map: dict[str, str]) -> None: # noqa: F821 +def _assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additional_imports_module_attr_map: dict[str, str]) -> None: # Safety check that user has passed in all required imports: tool_name = tool.__class__.__name__ current_class_imports = {tool_name} @@ -87,7 +99,7 @@ def _assert_all_classes_are_imported(tool: Union["LangChainBaseTool"], additiona raise RuntimeError(err_msg) -def _find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]: # noqa: F821 +def _find_required_class_names_for_import(obj: Union["LangChainBaseTool", BaseModel]) -> list[str]: """ Finds all the class names for required imports when instantiating the `obj`. NOTE: This does not return the full import path, only the class name. @@ -225,7 +237,7 @@ def _parse_letta_response_for_assistant_message( async def async_execute_send_message_to_agent( - sender_agent: "Agent", # noqa: F821 + sender_agent: "Agent", messages: List[MessageCreate], other_agent_id: str, log_prefix: str, @@ -256,7 +268,7 @@ async def async_execute_send_message_to_agent( def execute_send_message_to_agent( - sender_agent: "Agent", # noqa: F821 + sender_agent: "Agent", messages: List[MessageCreate], other_agent_id: str, log_prefix: str, @@ -269,7 +281,7 @@ def execute_send_message_to_agent( async def _send_message_to_agent_no_stream( - server: "SyncServer", # noqa: F821 + server: "SyncServer", agent_id: str, actor: User, messages: List[MessageCreate], @@ -302,8 +314,8 @@ async def _send_message_to_agent_no_stream( async def _async_send_message_with_retries( - server: "SyncServer", # noqa: F821 - sender_agent: "Agent", # noqa: F821 + server: "SyncServer", + sender_agent: "Agent", target_agent_id: str, messages: List[MessageCreate], max_retries: int, @@ -353,7 +365,7 @@ async def _async_send_message_with_retries( def fire_and_forget_send_to_agent( - sender_agent: "Agent", # noqa: F821 + sender_agent: "Agent", messages: List[MessageCreate], other_agent_id: str, log_prefix: str, @@ -429,18 +441,18 @@ def fire_and_forget_send_to_agent( # 4) Try to schedule the coroutine in an existing loop, else spawn a thread try: loop = asyncio.get_running_loop() - # If we get here, a loop is running; schedule the coroutine in background - loop.create_task(background_task()) + task = loop.create_task(background_task()) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) except RuntimeError: - # Means no event loop is running in this thread run_in_background_thread(background_task()) async def _send_message_to_agents_matching_tags_async( - sender_agent: "Agent", # noqa: F821 - server: "SyncServer", # noqa: F821 + sender_agent: "Agent", + server: "SyncServer", messages: List[MessageCreate], - matching_agents: List["AgentState"], # noqa: F821 + matching_agents: List["AgentState"], ) -> List[str]: async def _send_single(agent_state): return await _async_send_message_with_retries( @@ -464,7 +476,7 @@ async def _send_message_to_agents_matching_tags_async( return final -async def _send_message_to_all_agents_in_group_async(sender_agent: "Agent", message: str) -> List[str]: # noqa: F821 +async def _send_message_to_all_agents_in_group_async(sender_agent: "Agent", message: str) -> List[str]: server = get_letta_server() augmented_message = ( @@ -522,7 +534,9 @@ def generate_model_from_args_json_schema(schema: Dict[str, Any]) -> Type[BaseMod return _create_model_from_schema(schema.get("title", "DynamicModel"), schema, nested_models) -def _create_model_from_schema(name: str, model_schema: Dict[str, Any], nested_models: Dict[str, Type[BaseModel]] = None) -> Type[BaseModel]: +def _create_model_from_schema( + name: str, model_schema: Dict[str, Any], nested_models: Dict[str, Type[BaseModel]] | None = None +) -> Type[BaseModel]: fields = {} for field_name, field_schema in model_schema["properties"].items(): field_type = _get_field_type(field_schema, nested_models) @@ -533,7 +547,7 @@ def _create_model_from_schema(name: str, model_schema: Dict[str, Any], nested_mo return create_model(name, **fields) -def _get_field_type(field_schema: Dict[str, Any], nested_models: Dict[str, Type[BaseModel]] = None) -> Any: +def _get_field_type(field_schema: Dict[str, Any], nested_models: Dict[str, Type[BaseModel]] | None = None) -> Any: """Helper to convert JSON schema types to Python types.""" if field_schema.get("type") == "string": return str diff --git a/letta/functions/schema_generator.py b/letta/functions/schema_generator.py index de811488..79ab66a9 100644 --- a/letta/functions/schema_generator.py +++ b/letta/functions/schema_generator.py @@ -96,7 +96,7 @@ def type_to_json_schema_type(py_type) -> dict: # Handle array types origin = get_origin(py_type) - if py_type == list or origin in (list, List): + if py_type is list or origin in (list, List): args = get_args(py_type) if len(args) == 0: # is this correct @@ -142,7 +142,7 @@ def type_to_json_schema_type(py_type) -> dict: } # Handle object types - if py_type == dict or origin in (dict, Dict): + if py_type is dict or origin in (dict, Dict): args = get_args(py_type) if not args: # Generic dict without type arguments diff --git a/letta/functions/schema_validator.py b/letta/functions/schema_validator.py index dd99fd04..82c921c2 100644 --- a/letta/functions/schema_validator.py +++ b/letta/functions/schema_validator.py @@ -56,7 +56,7 @@ def validate_complete_json_schema(schema: Dict[str, Any]) -> Tuple[SchemaHealth, """ if obj_schema.get("type") != "object": return False - props = obj_schema.get("properties", {}) + obj_schema.get("properties", {}) required = obj_schema.get("required", []) additional = obj_schema.get("additionalProperties", True) diff --git a/letta/groups/dynamic_multi_agent.py b/letta/groups/dynamic_multi_agent.py index 926d9eff..1e3368ff 100644 --- a/letta/groups/dynamic_multi_agent.py +++ b/letta/groups/dynamic_multi_agent.py @@ -1,4 +1,7 @@ -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from letta.agents.letta_agent import LettaAgent as Agent from letta.agents.base_agent import BaseAgent from letta.agents.letta_agent import LettaAgent @@ -92,7 +95,7 @@ class DynamicMultiAgent(BaseAgent): # Parse manager response responses = Message.to_letta_messages_from_list(manager_agent.last_response_messages) - assistant_message = [response for response in responses if response.message_type == "assistant_message"][0] + assistant_message = next(response for response in responses if response.message_type == "assistant_message") for name, agent_id in [(agents[agent_id].agent_state.name, agent_id) for agent_id in agent_id_options]: if name.lower() in assistant_message.content.lower(): speaker_id = agent_id @@ -177,7 +180,7 @@ class DynamicMultiAgent(BaseAgent): return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count) - def load_manager_agent(self) -> Agent: # noqa: F821 + def load_manager_agent(self) -> Agent: for participant_agent_id in self.agent_ids: participant_agent_state = self.agent_manager.get_agent_by_id(agent_id=participant_agent_id, actor=self.user) participant_persona_block = participant_agent_state.memory.get_block(label="persona") diff --git a/letta/groups/helpers.py b/letta/groups/helpers.py index 1b8ec916..386a6a3f 100644 --- a/letta/groups/helpers.py +++ b/letta/groups/helpers.py @@ -98,7 +98,7 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str elif isinstance(content, ImageContent): messages.append(f"{message.name or 'user'}: [Image Here]") return "\n".join(messages) - except: + except Exception: if message.content and len(message.content) > 0: return f"{message.name or 'user'}: {message.content[0].text}" return None diff --git a/letta/groups/sleeptime_multi_agent_v2.py b/letta/groups/sleeptime_multi_agent_v2.py index 936136a5..65b33632 100644 --- a/letta/groups/sleeptime_multi_agent_v2.py +++ b/letta/groups/sleeptime_multi_agent_v2.py @@ -212,7 +212,7 @@ class SleeptimeMultiAgentV2(BaseAgent): group_id=self.group.id, last_processed_message_id=last_response_messages[-1].id, actor=self.actor ) for sleeptime_agent_id in self.group.agent_ids: - run_id = await self._issue_background_task( + await self._issue_background_task( sleeptime_agent_id, last_response_messages, last_processed_message_id, diff --git a/letta/helpers/converters.py b/letta/helpers/converters.py index a3e7a581..257498bd 100644 --- a/letta/helpers/converters.py +++ b/letta/helpers/converters.py @@ -1,4 +1,7 @@ -from typing import Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +if TYPE_CHECKING: + from letta.services.summarizer.summarizer_config import CompactionSettings import numpy as np from anthropic.types.beta.messages import BetaMessageBatch, BetaMessageBatchIndividualResponse @@ -113,7 +116,7 @@ def deserialize_embedding_config(data: Optional[Dict]) -> Optional[EmbeddingConf # -------------------------- -def serialize_compaction_settings(config: Union[Optional["CompactionSettings"], Dict]) -> Optional[Dict]: # noqa: F821 +def serialize_compaction_settings(config: Union[Optional["CompactionSettings"], Dict]) -> Optional[Dict]: """Convert a CompactionSettings object into a JSON-serializable dictionary.""" if config: # Import here to avoid circular dependency @@ -124,7 +127,7 @@ def serialize_compaction_settings(config: Union[Optional["CompactionSettings"], return config -def deserialize_compaction_settings(data: Optional[Dict]) -> Optional["CompactionSettings"]: # noqa: F821 +def deserialize_compaction_settings(data: Optional[Dict]) -> Optional["CompactionSettings"]: """Convert a dictionary back into a CompactionSettings object.""" if data: # Import here to avoid circular dependency diff --git a/letta/helpers/pinecone_utils.py b/letta/helpers/pinecone_utils.py index d85727e5..409d7ddc 100644 --- a/letta/helpers/pinecone_utils.py +++ b/letta/helpers/pinecone_utils.py @@ -306,7 +306,9 @@ async def search_pinecone_index(query: str, limit: int, filter: Dict[str, Any], @pinecone_retry() @trace_method -async def list_pinecone_index_for_files(file_id: str, actor: User, limit: int = None, pagination_token: str = None) -> List[str]: +async def list_pinecone_index_for_files( + file_id: str, actor: User, limit: int | None = None, pagination_token: str | None = None +) -> List[str]: if not PINECONE_AVAILABLE: raise ImportError("Pinecone is not available. Please install pinecone to use this feature.") diff --git a/letta/helpers/tool_execution_helper.py b/letta/helpers/tool_execution_helper.py index a32cd0a7..3d6a7fa1 100644 --- a/letta/helpers/tool_execution_helper.py +++ b/letta/helpers/tool_execution_helper.py @@ -201,7 +201,7 @@ def add_pre_execution_message(tool_schema: Dict[str, Any], description: Optional # Ensure pre-execution message is the first required field if PRE_EXECUTION_MESSAGE_ARG not in required: - required = [PRE_EXECUTION_MESSAGE_ARG] + required + required = [PRE_EXECUTION_MESSAGE_ARG, *required] # Update the schema with ordered properties and required list schema["parameters"] = { diff --git a/letta/helpers/tpuf_client.py b/letta/helpers/tpuf_client.py index ab6414a4..4976080a 100644 --- a/letta/helpers/tpuf_client.py +++ b/letta/helpers/tpuf_client.py @@ -6,7 +6,11 @@ import logging import random from datetime import datetime, timezone from functools import wraps -from typing import Any, Callable, List, Optional, Tuple, TypeVar +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, TypeVar + +if TYPE_CHECKING: + from letta.schemas.tool import Tool as PydanticTool + from letta.schemas.user import User as PydanticUser import httpx @@ -95,7 +99,6 @@ def async_retry_with_backoff( async def wrapper(*args, **kwargs) -> Any: num_retries = 0 delay = initial_delay - last_error: Optional[Exception] = None while True: try: @@ -106,7 +109,6 @@ def async_retry_with_backoff( # Not a transient error, re-raise immediately raise - last_error = e num_retries += 1 # Log the retry attempt @@ -161,11 +163,11 @@ def _run_turbopuffer_write_in_thread( api_key: str, region: str, namespace_name: str, - upsert_columns: dict = None, - deletes: list = None, - delete_by_filter: tuple = None, + upsert_columns: dict | None = None, + deletes: list | None = None, + delete_by_filter: tuple | None = None, distance_metric: str = "cosine_distance", - schema: dict = None, + schema: dict | None = None, ): """ Sync wrapper to run turbopuffer write in isolated event loop. @@ -229,7 +231,7 @@ class TurbopufferClient: embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE, ) - def __init__(self, api_key: str = None, region: str = None): + def __init__(self, api_key: str | None = None, region: str | None = None): """Initialize Turbopuffer client.""" self.api_key = api_key or settings.tpuf_api_key self.region = region or settings.tpuf_region @@ -244,7 +246,7 @@ class TurbopufferClient: raise ValueError("Turbopuffer API key not provided") @trace_method - async def _generate_embeddings(self, texts: List[str], actor: "PydanticUser") -> List[List[float]]: # noqa: F821 + async def _generate_embeddings(self, texts: List[str], actor: "PydanticUser") -> List[List[float]]: """Generate embeddings using the default embedding configuration. Args: @@ -311,7 +313,7 @@ class TurbopufferClient: return namespace_name - def _extract_tool_text(self, tool: "PydanticTool") -> str: # noqa: F821 + def _extract_tool_text(self, tool: "PydanticTool") -> str: """Extract searchable text from a tool for embedding. Combines name, description, and JSON schema into a structured format @@ -361,9 +363,9 @@ class TurbopufferClient: @async_retry_with_backoff() async def insert_tools( self, - tools: List["PydanticTool"], # noqa: F821 + tools: List["PydanticTool"], organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", ) -> bool: """Insert tools into Turbopuffer. @@ -456,7 +458,7 @@ class TurbopufferClient: text_chunks: List[str], passage_ids: List[str], organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", tags: Optional[List[str]] = None, created_at: Optional[datetime] = None, embeddings: Optional[List[List[float]]] = None, @@ -607,7 +609,7 @@ class TurbopufferClient: message_texts: List[str], message_ids: List[str], organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", roles: List[MessageRole], created_ats: List[datetime], project_id: Optional[str] = None, @@ -867,7 +869,7 @@ class TurbopufferClient: async def query_passages( self, archive_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", query_text: Optional[str] = None, search_mode: str = "vector", # "vector", "fts", "hybrid" top_k: int = 10, @@ -1012,7 +1014,7 @@ class TurbopufferClient: self, agent_id: str, organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", query_text: Optional[str] = None, search_mode: str = "vector", # "vector", "fts", "hybrid", "timestamp" top_k: int = 10, @@ -1188,7 +1190,7 @@ class TurbopufferClient: async def query_messages_by_org_id( self, organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", query_text: Optional[str] = None, search_mode: str = "hybrid", # "vector", "fts", "hybrid" top_k: int = 10, @@ -1654,7 +1656,7 @@ class TurbopufferClient: file_id: str, text_chunks: List[str], organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", created_at: Optional[datetime] = None, ) -> List[PydanticPassage]: """Insert file passages into Turbopuffer using org-scoped namespace. @@ -1767,7 +1769,7 @@ class TurbopufferClient: self, source_ids: List[str], organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", query_text: Optional[str] = None, search_mode: str = "vector", # "vector", "fts", "hybrid" top_k: int = 10, @@ -1991,7 +1993,7 @@ class TurbopufferClient: async def query_tools( self, organization_id: str, - actor: "PydanticUser", # noqa: F821 + actor: "PydanticUser", query_text: Optional[str] = None, search_mode: str = "hybrid", # "vector", "fts", "hybrid", "timestamp" top_k: int = 50, diff --git a/letta/interface.py b/letta/interface.py index 7e146b07..a290d893 100644 --- a/letta/interface.py +++ b/letta/interface.py @@ -136,7 +136,7 @@ class CLIInterface(AgentInterface): else: try: msg_json = json_loads(msg) - except: + except Exception: printd(f"{CLI_WARNING_PREFIX}failed to parse user message into json") printd_user_message("🧑", msg) return diff --git a/letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py b/letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py index 14b9cd10..4f893407 100644 --- a/letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +++ b/letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py @@ -3,7 +3,12 @@ import json from collections.abc import AsyncGenerator from datetime import datetime, timezone from enum import Enum -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + from letta.schemas.usage import LettaUsageStatistics from anthropic import AsyncStream from anthropic.types.beta import ( @@ -146,7 +151,7 @@ class SimpleAnthropicStreamingInterface: return tool_calls[0] return None - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -232,7 +237,7 @@ class SimpleAnthropicStreamingInterface: async def process( self, stream: AsyncStream[BetaRawMessageStreamEvent], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: prev_message_type = None message_index = 0 @@ -287,7 +292,7 @@ class SimpleAnthropicStreamingInterface: async def _process_event( self, event: BetaRawMessageStreamEvent, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: diff --git a/letta/interfaces/anthropic_streaming_interface.py b/letta/interfaces/anthropic_streaming_interface.py index a4101bbd..0b31dbe1 100644 --- a/letta/interfaces/anthropic_streaming_interface.py +++ b/letta/interfaces/anthropic_streaming_interface.py @@ -3,7 +3,12 @@ import json from collections.abc import AsyncGenerator from datetime import datetime, timezone from enum import Enum -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + from letta.schemas.usage import LettaUsageStatistics from anthropic import AsyncStream from anthropic.types.beta import ( @@ -116,7 +121,7 @@ class AnthropicStreamingInterface: # Attempt to use OptimisticJSONParser to handle incomplete/malformed JSON try: tool_input = self.json_parser.parse(args_str) - except: + except Exception: logger.warning( f"Failed to decode tool call arguments for tool_call_id={self.tool_call_id}, " f"name={self.tool_call_name}. Raw input: {args_str!r}. Error: {e}" @@ -128,7 +133,7 @@ class AnthropicStreamingInterface: arguments = str(json.dumps(tool_input, indent=2)) return ToolCall(id=self.tool_call_id, function=FunctionCall(arguments=arguments, name=self.tool_call_name)) - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -222,7 +227,7 @@ class AnthropicStreamingInterface: async def process( self, stream: AsyncStream[BetaRawMessageStreamEvent], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: prev_message_type = None message_index = 0 @@ -276,7 +281,7 @@ class AnthropicStreamingInterface: async def _process_event( self, event: BetaRawMessageStreamEvent, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: @@ -650,7 +655,7 @@ class SimpleAnthropicStreamingInterface: # Attempt to use OptimisticJSONParser to handle incomplete/malformed JSON try: tool_input = self.json_parser.parse(args_str) - except: + except Exception: logger.warning( f"Failed to decode tool call arguments for tool_call_id={self.tool_call_id}, " f"name={self.tool_call_name}. Raw input: {args_str!r}. Error: {e}" @@ -662,7 +667,7 @@ class SimpleAnthropicStreamingInterface: arguments = str(json.dumps(tool_input, indent=2)) return ToolCall(id=self.tool_call_id, function=FunctionCall(arguments=arguments, name=self.tool_call_name)) - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -754,7 +759,7 @@ class SimpleAnthropicStreamingInterface: async def process( self, stream: AsyncStream[BetaRawMessageStreamEvent], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: prev_message_type = None message_index = 0 @@ -803,7 +808,7 @@ class SimpleAnthropicStreamingInterface: async def _process_event( self, event: BetaRawMessageStreamEvent, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: diff --git a/letta/interfaces/gemini_streaming_interface.py b/letta/interfaces/gemini_streaming_interface.py index 9c3ef633..5cc026b7 100644 --- a/letta/interfaces/gemini_streaming_interface.py +++ b/letta/interfaces/gemini_streaming_interface.py @@ -3,7 +3,12 @@ import base64 import json from collections.abc import AsyncGenerator from datetime import datetime, timezone -from typing import AsyncIterator, List, Optional +from typing import TYPE_CHECKING, AsyncIterator, List, Optional + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + from letta.schemas.usage import LettaUsageStatistics from google.genai.types import ( GenerateContentResponse, @@ -124,7 +129,7 @@ class SimpleGeminiStreamingInterface: """Return all finalized tool calls collected during this message (parallel supported).""" return list(self.collected_tool_calls) - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -148,7 +153,7 @@ class SimpleGeminiStreamingInterface: async def process( self, stream: AsyncIterator[GenerateContentResponse], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: """ Iterates over the Gemini stream, yielding SSE events. @@ -202,7 +207,7 @@ class SimpleGeminiStreamingInterface: async def _process_event( self, event: GenerateContentResponse, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: diff --git a/letta/interfaces/openai_streaming_interface.py b/letta/interfaces/openai_streaming_interface.py index 69abde67..5f8aa3fb 100644 --- a/letta/interfaces/openai_streaming_interface.py +++ b/letta/interfaces/openai_streaming_interface.py @@ -1,7 +1,12 @@ import asyncio from collections.abc import AsyncGenerator from datetime import datetime, timezone -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + from letta.schemas.usage import LettaUsageStatistics from openai import AsyncStream from openai.types.chat.chat_completion_chunk import ChatCompletionChunk @@ -194,7 +199,7 @@ class OpenAIStreamingInterface: function=FunctionCall(arguments=self._get_current_function_arguments(), name=function_name), ) - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -219,7 +224,7 @@ class OpenAIStreamingInterface: async def process( self, stream: AsyncStream[ChatCompletionChunk], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: """ Iterates over the OpenAI stream, yielding SSE events. @@ -307,7 +312,7 @@ class OpenAIStreamingInterface: async def _process_chunk( self, chunk: ChatCompletionChunk, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: @@ -471,7 +476,7 @@ class OpenAIStreamingInterface: # Minimal, robust extraction: only emit the value of "message". # If we buffered a prefix while name was streaming, feed it first. if self._function_args_buffer_parts: - payload = "".join(self._function_args_buffer_parts + [tool_call.function.arguments]) + payload = "".join([*self._function_args_buffer_parts, tool_call.function.arguments]) self._function_args_buffer_parts = None else: payload = tool_call.function.arguments @@ -498,7 +503,7 @@ class OpenAIStreamingInterface: # if the previous chunk had arguments but we needed to flush name if self._function_args_buffer_parts: # In this case, we should release the buffer + new data at once - combined_chunk = "".join(self._function_args_buffer_parts + [updates_main_json]) + combined_chunk = "".join([*self._function_args_buffer_parts, updates_main_json]) if prev_message_type and prev_message_type != "tool_call_message": message_index += 1 if self._get_function_name_buffer() in self.requires_approval_tools: @@ -588,7 +593,7 @@ class SimpleOpenAIStreamingInterface: messages: Optional[list] = None, tools: Optional[list] = None, requires_approval_tools: list = [], - model: str = None, + model: str | None = None, run_id: str | None = None, step_id: str | None = None, cancellation_event: Optional["asyncio.Event"] = None, @@ -639,7 +644,6 @@ class SimpleOpenAIStreamingInterface: def get_content(self) -> list[TextContent | OmittedReasoningContent | ReasoningContent]: shown_omitted = False - concat_content = "" merged_messages = [] reasoning_content = [] concat_content_parts: list[str] = [] @@ -694,7 +698,7 @@ class SimpleOpenAIStreamingInterface: raise ValueError("No tool calls available") return calls[0] - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -719,7 +723,7 @@ class SimpleOpenAIStreamingInterface: async def process( self, stream: AsyncStream[ChatCompletionChunk], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: """ Iterates over the OpenAI stream, yielding SSE events. @@ -833,7 +837,7 @@ class SimpleOpenAIStreamingInterface: async def _process_chunk( self, chunk: ChatCompletionChunk, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: @@ -984,7 +988,7 @@ class SimpleOpenAIResponsesStreamingInterface: messages: Optional[list] = None, tools: Optional[list] = None, requires_approval_tools: list = [], - model: str = None, + model: str | None = None, run_id: str | None = None, step_id: str | None = None, cancellation_event: Optional["asyncio.Event"] = None, @@ -1120,7 +1124,7 @@ class SimpleOpenAIResponsesStreamingInterface: raise ValueError("No tool calls available") return calls[0] - def get_usage_statistics(self) -> "LettaUsageStatistics": # noqa: F821 + def get_usage_statistics(self) -> "LettaUsageStatistics": """Extract usage statistics from accumulated streaming data. Returns: @@ -1141,7 +1145,7 @@ class SimpleOpenAIResponsesStreamingInterface: async def process( self, stream: AsyncStream[ResponseStreamEvent], - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: """ Iterates over the OpenAI stream, yielding SSE events. @@ -1227,7 +1231,7 @@ class SimpleOpenAIResponsesStreamingInterface: async def _process_event( self, event: ResponseStreamEvent, - ttft_span: Optional["Span"] = None, # noqa: F821 + ttft_span: Optional["Span"] = None, prev_message_type: Optional[str] = None, message_index: int = 0, ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]: @@ -1250,8 +1254,6 @@ class SimpleOpenAIResponsesStreamingInterface: if isinstance(new_event_item, ResponseReasoningItem): # Look for summary delta, or encrypted_content summary = new_event_item.summary - content = new_event_item.content # NOTE: always none - encrypted_content = new_event_item.encrypted_content # TODO change to summarize reasoning message, but we need to figure out the streaming indices of summary problem concat_summary = "".join([s.text for s in summary]) if concat_summary != "": @@ -1390,7 +1392,6 @@ class SimpleOpenAIResponsesStreamingInterface: # NOTE: is this inclusive of the deltas? # If not, we should add it to the rolling summary_index = event.summary_index - text = event.text return # Reasoning summary streaming @@ -1432,7 +1433,6 @@ class SimpleOpenAIResponsesStreamingInterface: # Assistant message streaming elif isinstance(event, ResponseTextDoneEvent): # NOTE: inclusive, can skip - text = event.text return # Assistant message done @@ -1447,7 +1447,7 @@ class SimpleOpenAIResponsesStreamingInterface: delta = event.delta # Resolve tool_call_id/name using output_index or item_id - resolved_call_id, resolved_name, out_idx, item_id = self._resolve_mapping_for_delta(event) + resolved_call_id, resolved_name, _out_idx, _item_id = self._resolve_mapping_for_delta(event) # Fallback to last seen tool name for approval routing if mapping name missing if not resolved_name: @@ -1493,7 +1493,6 @@ class SimpleOpenAIResponsesStreamingInterface: # Function calls elif isinstance(event, ResponseFunctionCallArgumentsDoneEvent): # NOTE: inclusive - full_args = event.arguments return # Generic diff --git a/letta/jobs/scheduler.py b/letta/jobs/scheduler.py index 8b1bac10..3ee4d136 100644 --- a/letta/jobs/scheduler.py +++ b/letta/jobs/scheduler.py @@ -94,7 +94,7 @@ async def _try_acquire_lock_and_start_scheduler(server: SyncServer) -> bool: if scheduler.running: try: scheduler.shutdown(wait=False) - except: + except Exception: pass return False finally: diff --git a/letta/llm_api/anthropic_client.py b/letta/llm_api/anthropic_client.py index 36c4868f..08b06f2c 100644 --- a/letta/llm_api/anthropic_client.py +++ b/letta/llm_api/anthropic_client.py @@ -406,7 +406,7 @@ class AnthropicClient(LLMClientBase): for agent_id in agent_messages_mapping } - client = await self._get_anthropic_client_async(list(agent_llm_config_mapping.values())[0], async_client=True) + client = await self._get_anthropic_client_async(next(iter(agent_llm_config_mapping.values())), async_client=True) anthropic_requests = [ Request(custom_id=agent_id, params=MessageCreateParamsNonStreaming(**params)) for agent_id, params in requests.items() @@ -599,7 +599,7 @@ class AnthropicClient(LLMClientBase): # Special case for summarization path tools_for_request = None tool_choice = None - elif self.is_reasoning_model(llm_config) and llm_config.enable_reasoner or agent_type == AgentType.letta_v1_agent: + elif (self.is_reasoning_model(llm_config) and llm_config.enable_reasoner) or agent_type == AgentType.letta_v1_agent: # NOTE: reasoning models currently do not allow for `any` # NOTE: react agents should always have at least auto on, since the precense/absense of tool calls controls chaining if agent_type == AgentType.split_thread_agent and force_tool_call is not None: @@ -785,7 +785,9 @@ class AnthropicClient(LLMClientBase): return data - async def count_tokens(self, messages: List[dict] = None, model: str = None, tools: List[OpenAITool] = None) -> int: + async def count_tokens( + self, messages: List[dict] | None = None, model: str | None = None, tools: List[OpenAITool] | None = None + ) -> int: logging.getLogger("httpx").setLevel(logging.WARNING) # Use the default client; token counting is lightweight and does not require BYOK overrides client = anthropic.AsyncAnthropic() @@ -1104,7 +1106,7 @@ class AnthropicClient(LLMClientBase): if isinstance(e, anthropic.APIStatusError): logger.warning(f"[Anthropic] API status error: {str(e)}") - if hasattr(e, "status_code") and e.status_code == 402 or is_insufficient_credits_message(str(e)): + if (hasattr(e, "status_code") and e.status_code == 402) or is_insufficient_credits_message(str(e)): msg = str(e) return LLMInsufficientCreditsError( message=f"Insufficient credits (BYOK): {msg}" if is_byok else f"Insufficient credits: {msg}", @@ -1247,7 +1249,7 @@ class AnthropicClient(LLMClientBase): args_json = json.loads(arguments) if not isinstance(args_json, dict): raise LLMServerError("Expected parseable json object for arguments") - except: + except Exception: arguments = str(tool_input["function"]["arguments"]) else: arguments = json.dumps(tool_input, indent=2) @@ -1539,7 +1541,7 @@ def is_heartbeat(message: dict, is_ping: bool = False) -> bool: try: message_json = json.loads(message["content"]) - except: + except Exception: return False # Check if message_json is a dict (not int, str, list, etc.) diff --git a/letta/llm_api/google_vertex_client.py b/letta/llm_api/google_vertex_client.py index 5dac8cf4..13dc5936 100644 --- a/letta/llm_api/google_vertex_client.py +++ b/letta/llm_api/google_vertex_client.py @@ -302,7 +302,7 @@ class GoogleVertexClient(LLMClientBase): for item_schema in schema_part[key]: self._clean_google_ai_schema_properties(item_schema) - def _resolve_json_schema_refs(self, schema: dict, defs: dict = None) -> dict: + def _resolve_json_schema_refs(self, schema: dict, defs: dict | None = None) -> dict: """ Recursively resolve $ref in JSON schema by inlining definitions. Google GenAI SDK does not support $ref. @@ -1057,7 +1057,9 @@ class GoogleVertexClient(LLMClientBase): # Fallback to base implementation for other errors return super().handle_llm_error(e, llm_config=llm_config) - async def count_tokens(self, messages: List[dict] = None, model: str = None, tools: List[OpenAITool] = None) -> int: + async def count_tokens( + self, messages: List[dict] | None = None, model: str | None = None, tools: List[OpenAITool] | None = None + ) -> int: """ Count tokens for the given messages and tools using the Gemini token counting API. diff --git a/letta/llm_api/openai.py b/letta/llm_api/openai.py index ae4e3451..deda50e0 100644 --- a/letta/llm_api/openai.py +++ b/letta/llm_api/openai.py @@ -86,7 +86,7 @@ async def openai_get_model_list_async( # Handle HTTP errors (e.g., response 4XX, 5XX) try: error_response = http_err.response.json() - except: + except Exception: error_response = {"status_code": http_err.response.status_code, "text": http_err.response.text} logger.debug(f"Got HTTPError, exception={http_err}, response={error_response}") raise http_err diff --git a/letta/llm_api/openai_client.py b/letta/llm_api/openai_client.py index b0a79d71..82672cc9 100644 --- a/letta/llm_api/openai_client.py +++ b/letta/llm_api/openai_client.py @@ -106,7 +106,7 @@ def accepts_developer_role(model: str) -> bool: See: https://community.openai.com/t/developer-role-not-accepted-for-o1-o1-mini-o3-mini/1110750/7 """ - if is_openai_reasoning_model(model) and "o1-mini" not in model or "o1-preview" in model: + if (is_openai_reasoning_model(model) and "o1-mini" not in model) or "o1-preview" in model: return True else: return False @@ -459,7 +459,7 @@ class OpenAIClient(LLMClientBase): if is_openrouter: try: model = llm_config.handle.split("/", 1)[-1] - except: + except Exception: # don't raise error since this isn't robust against edge cases pass @@ -747,7 +747,6 @@ class OpenAIClient(LLMClientBase): finish_reason = None # Optionally capture reasoning presence - found_reasoning = False for out in outputs: out_type = (out or {}).get("type") if out_type == "message": @@ -758,7 +757,6 @@ class OpenAIClient(LLMClientBase): if text_val: assistant_text_parts.append(text_val) elif out_type == "reasoning": - found_reasoning = True reasoning_summary_parts = [part.get("text") for part in out.get("summary")] reasoning_content_signature = out.get("encrypted_content") elif out_type == "function_call": diff --git a/letta/local_llm/chat_completion_proxy.py b/letta/local_llm/chat_completion_proxy.py index 1129b125..668938c3 100644 --- a/letta/local_llm/chat_completion_proxy.py +++ b/letta/local_llm/chat_completion_proxy.py @@ -16,7 +16,7 @@ from letta.local_llm.llamacpp.api import get_llamacpp_completion from letta.local_llm.llm_chat_completion_wrappers import simple_summary_wrapper from letta.local_llm.lmstudio.api import get_lmstudio_completion, get_lmstudio_completion_chatcompletions from letta.local_llm.ollama.api import get_ollama_completion -from letta.local_llm.utils import count_tokens, get_available_wrappers +from letta.local_llm.utils import get_available_wrappers from letta.local_llm.vllm.api import get_vllm_completion from letta.local_llm.webui.api import get_webui_completion from letta.local_llm.webui.legacy_api import get_webui_completion as get_webui_completion_legacy @@ -177,7 +177,7 @@ def get_chat_completion( raise LocalLLMError( f"Invalid endpoint type {endpoint_type}, please set variable depending on your backend (webui, lmstudio, llamacpp, koboldcpp)" ) - except requests.exceptions.ConnectionError as e: + except requests.exceptions.ConnectionError: raise LocalLLMConnectionError(f"Unable to connect to endpoint {endpoint}") attributes = usage if isinstance(usage, dict) else {"usage": usage} @@ -207,10 +207,12 @@ def get_chat_completion( if usage["prompt_tokens"] is None: printd("usage dict was missing prompt_tokens, computing on-the-fly...") - usage["prompt_tokens"] = count_tokens(prompt) + # Approximate token count: bytes / 4 + usage["prompt_tokens"] = len(prompt.encode("utf-8")) // 4 # NOTE: we should compute on-the-fly anyways since we might have to correct for errors during JSON parsing - usage["completion_tokens"] = count_tokens(json_dumps(chat_completion_result)) + # Approximate token count: bytes / 4 + usage["completion_tokens"] = len(json_dumps(chat_completion_result).encode("utf-8")) // 4 """ if usage["completion_tokens"] is None: printd(f"usage dict was missing completion_tokens, computing on-the-fly...") diff --git a/letta/local_llm/grammars/gbnf_grammar_generator.py b/letta/local_llm/grammars/gbnf_grammar_generator.py index 536bf8e2..0b35e44f 100644 --- a/letta/local_llm/grammars/gbnf_grammar_generator.py +++ b/letta/local_llm/grammars/gbnf_grammar_generator.py @@ -5,7 +5,7 @@ from copy import copy from enum import Enum from inspect import getdoc, isclass from types import NoneType -from typing import Any, Callable, List, Optional, Tuple, Type, Union, _GenericAlias, get_args, get_origin +from typing import Any, Callable, List, Optional, Tuple, Type, Union, _GenericAlias, get_args, get_origin # type: ignore[attr-defined] from docstring_parser import parse from pydantic import BaseModel, create_model @@ -58,13 +58,13 @@ def map_pydantic_type_to_gbnf(pydantic_type: Type[Any]) -> str: elif isclass(pydantic_type) and issubclass(pydantic_type, BaseModel): return format_model_and_field_name(pydantic_type.__name__) - elif get_origin(pydantic_type) == list: + elif get_origin(pydantic_type) is list: element_type = get_args(pydantic_type)[0] return f"{map_pydantic_type_to_gbnf(element_type)}-list" - elif get_origin(pydantic_type) == set: + elif get_origin(pydantic_type) is set: element_type = get_args(pydantic_type)[0] return f"{map_pydantic_type_to_gbnf(element_type)}-set" - elif get_origin(pydantic_type) == Union: + elif get_origin(pydantic_type) is Union: union_types = get_args(pydantic_type) union_rules = [map_pydantic_type_to_gbnf(ut) for ut in union_types] return f"union-{'-or-'.join(union_rules)}" @@ -73,7 +73,7 @@ def map_pydantic_type_to_gbnf(pydantic_type: Type[Any]) -> str: return f"optional-{map_pydantic_type_to_gbnf(element_type)}" elif isclass(pydantic_type): return f"{PydanticDataType.CUSTOM_CLASS.value}-{format_model_and_field_name(pydantic_type.__name__)}" - elif get_origin(pydantic_type) == dict: + elif get_origin(pydantic_type) is dict: key_type, value_type = get_args(pydantic_type) return f"custom-dict-key-type-{format_model_and_field_name(map_pydantic_type_to_gbnf(key_type))}-value-type-{format_model_and_field_name(map_pydantic_type_to_gbnf(value_type))}" else: @@ -299,7 +299,7 @@ def generate_gbnf_rule_for_type( enum_rule = f"{model_name}-{field_name} ::= {' | '.join(enum_values)}" rules.append(enum_rule) gbnf_type, rules = model_name + "-" + field_name, rules - elif get_origin(field_type) == list: # Array + elif get_origin(field_type) is list: # Array element_type = get_args(field_type)[0] element_rule_name, additional_rules = generate_gbnf_rule_for_type( model_name, f"{field_name}-element", element_type, is_optional, processed_models, created_rules @@ -309,7 +309,7 @@ def generate_gbnf_rule_for_type( rules.append(array_rule) gbnf_type, rules = model_name + "-" + field_name, rules - elif get_origin(field_type) == set or field_type == set: # Array + elif get_origin(field_type) is set or field_type is set: # Array element_type = get_args(field_type)[0] element_rule_name, additional_rules = generate_gbnf_rule_for_type( model_name, f"{field_name}-element", element_type, is_optional, processed_models, created_rules @@ -320,7 +320,7 @@ def generate_gbnf_rule_for_type( gbnf_type, rules = model_name + "-" + field_name, rules elif gbnf_type.startswith("custom-class-"): - nested_model_rules, field_types = get_members_structure(field_type, gbnf_type) + nested_model_rules, _field_types = get_members_structure(field_type, gbnf_type) rules.append(nested_model_rules) elif gbnf_type.startswith("custom-dict-"): key_type, value_type = get_args(field_type) @@ -502,15 +502,15 @@ def generate_gbnf_grammar(model: Type[BaseModel], processed_models: set, created model_rule += '"\\n" ws "}"' model_rule += '"\\n" markdown-code-block' has_special_string = True - all_rules = [model_rule] + nested_rules + all_rules = [model_rule, *nested_rules] return all_rules, has_special_string def generate_gbnf_grammar_from_pydantic_models( models: List[Type[BaseModel]], - outer_object_name: str = None, - outer_object_content: str = None, + outer_object_name: str | None = None, + outer_object_content: str | None = None, list_of_outputs: bool = False, add_inner_thoughts: bool = False, allow_only_inner_thoughts: bool = False, @@ -704,11 +704,11 @@ def generate_markdown_documentation( # continue if isclass(field_type) and issubclass(field_type, BaseModel): pyd_models.append((field_type, False)) - if get_origin(field_type) == list: + if get_origin(field_type) is list: element_type = get_args(field_type)[0] if isclass(element_type) and issubclass(element_type, BaseModel): pyd_models.append((element_type, False)) - if get_origin(field_type) == Union: + if get_origin(field_type) is Union: element_types = get_args(field_type) for element_type in element_types: if isclass(element_type) and issubclass(element_type, BaseModel): @@ -747,14 +747,14 @@ def generate_field_markdown( field_info = model.model_fields.get(field_name) field_description = field_info.description if field_info and field_info.description else "" - if get_origin(field_type) == list: + if get_origin(field_type) is list: element_type = get_args(field_type)[0] field_text = f"{indent}{field_name} ({field_type.__name__} of {element_type.__name__})" if field_description != "": field_text += ": " else: field_text += "\n" - elif get_origin(field_type) == Union: + elif get_origin(field_type) is Union: element_types = get_args(field_type) types = [] for element_type in element_types: @@ -857,11 +857,11 @@ def generate_text_documentation( for name, field_type in model.__annotations__.items(): # if name == "markdown_code_block": # continue - if get_origin(field_type) == list: + if get_origin(field_type) is list: element_type = get_args(field_type)[0] if isclass(element_type) and issubclass(element_type, BaseModel): pyd_models.append((element_type, False)) - if get_origin(field_type) == Union: + if get_origin(field_type) is Union: element_types = get_args(field_type) for element_type in element_types: if isclass(element_type) and issubclass(element_type, BaseModel): @@ -905,14 +905,14 @@ def generate_field_text( field_info = model.model_fields.get(field_name) field_description = field_info.description if field_info and field_info.description else "" - if get_origin(field_type) == list: + if get_origin(field_type) is list: element_type = get_args(field_type)[0] field_text = f"{indent}{field_name} ({format_model_and_field_name(field_type.__name__)} of {format_model_and_field_name(element_type.__name__)})" if field_description != "": field_text += ":\n" else: field_text += "\n" - elif get_origin(field_type) == Union: + elif get_origin(field_type) is Union: element_types = get_args(field_type) types = [] for element_type in element_types: @@ -1015,8 +1015,8 @@ def generate_and_save_gbnf_grammar_and_documentation( pydantic_model_list, grammar_file_path="./generated_grammar.gbnf", documentation_file_path="./generated_grammar_documentation.md", - outer_object_name: str = None, - outer_object_content: str = None, + outer_object_name: str | None = None, + outer_object_content: str | None = None, model_prefix: str = "Output Model", fields_prefix: str = "Output Fields", list_of_outputs: bool = False, @@ -1049,8 +1049,8 @@ def generate_and_save_gbnf_grammar_and_documentation( def generate_gbnf_grammar_and_documentation( pydantic_model_list, - outer_object_name: str = None, - outer_object_content: str = None, + outer_object_name: str | None = None, + outer_object_content: str | None = None, model_prefix: str = "Output Model", fields_prefix: str = "Output Fields", list_of_outputs: bool = False, @@ -1087,8 +1087,8 @@ def generate_gbnf_grammar_and_documentation( def generate_gbnf_grammar_and_documentation_from_dictionaries( dictionaries: List[dict], - outer_object_name: str = None, - outer_object_content: str = None, + outer_object_name: str | None = None, + outer_object_content: str | None = None, model_prefix: str = "Output Model", fields_prefix: str = "Output Fields", list_of_outputs: bool = False, diff --git a/letta/local_llm/koboldcpp/api.py b/letta/local_llm/koboldcpp/api.py index e3aee69d..72c3cf06 100644 --- a/letta/local_llm/koboldcpp/api.py +++ b/letta/local_llm/koboldcpp/api.py @@ -1,7 +1,7 @@ from urllib.parse import urljoin from letta.local_llm.settings.settings import get_completions_settings -from letta.local_llm.utils import count_tokens, post_json_auth_request +from letta.local_llm.utils import post_json_auth_request KOBOLDCPP_API_SUFFIX = "/api/v1/generate" @@ -10,7 +10,8 @@ def get_koboldcpp_completion(endpoint, auth_type, auth_key, prompt, context_wind """See https://lite.koboldai.net/koboldcpp_api for API spec""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/local_llm/llamacpp/api.py b/letta/local_llm/llamacpp/api.py index e5d24eea..9ca7e1f6 100644 --- a/letta/local_llm/llamacpp/api.py +++ b/letta/local_llm/llamacpp/api.py @@ -1,7 +1,7 @@ from urllib.parse import urljoin from letta.local_llm.settings.settings import get_completions_settings -from letta.local_llm.utils import count_tokens, post_json_auth_request +from letta.local_llm.utils import post_json_auth_request LLAMACPP_API_SUFFIX = "/completion" @@ -10,7 +10,8 @@ def get_llamacpp_completion(endpoint, auth_type, auth_key, prompt, context_windo """See https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md for instructions on how to run the LLM web server""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/local_llm/llm_chat_completion_wrappers/airoboros.py b/letta/local_llm/llm_chat_completion_wrappers/airoboros.py index 544d11d4..5a3223fb 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/airoboros.py +++ b/letta/local_llm/llm_chat_completion_wrappers/airoboros.py @@ -130,12 +130,12 @@ class Airoboros21Wrapper(LLMChatCompletionWrapper): content_json = json_loads(message["content"]) content_simple = content_json["message"] prompt += f"\nUSER: {content_simple}" - except: + except Exception: prompt += f"\nUSER: {message['content']}" elif message["role"] == "assistant": prompt += f"\nASSISTANT: {message['content']}" # need to add the function call if there was one - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{create_function_call(message['function_call'])}" elif message["role"] in ["function", "tool"]: # TODO find a good way to add this @@ -348,7 +348,7 @@ class Airoboros21InnerMonologueWrapper(Airoboros21Wrapper): content_json = json_loads(message["content"]) content_simple = content_json["message"] prompt += f"\n{user_prefix}: {content_simple}" - except: + except Exception: prompt += f"\n{user_prefix}: {message['content']}" elif message["role"] == "assistant": # Support for AutoGen naming of agents @@ -360,7 +360,7 @@ class Airoboros21InnerMonologueWrapper(Airoboros21Wrapper): prompt += f"\n{assistant_prefix}:" # need to add the function call if there was one inner_thoughts = message["content"] - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{create_function_call(message['function_call'], inner_thoughts=inner_thoughts)}" elif message["role"] in ["function", "tool"]: # TODO find a good way to add this diff --git a/letta/local_llm/llm_chat_completion_wrappers/chatml.py b/letta/local_llm/llm_chat_completion_wrappers/chatml.py index 71589959..75c6b411 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/chatml.py +++ b/letta/local_llm/llm_chat_completion_wrappers/chatml.py @@ -143,9 +143,9 @@ class ChatMLInnerMonologueWrapper(LLMChatCompletionWrapper): # need to add the function call if there was one inner_thoughts = message["content"] - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{self._compile_function_call(message['function_call'], inner_thoughts=inner_thoughts)}" - elif "tool_calls" in message and message["tool_calls"]: + elif message.get("tool_calls"): for tool_call in message["tool_calls"]: prompt += f"\n{self._compile_function_call(tool_call['function'], inner_thoughts=inner_thoughts)}" else: @@ -163,14 +163,14 @@ class ChatMLInnerMonologueWrapper(LLMChatCompletionWrapper): try: user_msg_json = json_loads(message["content"]) user_msg_str = user_msg_json["message"] - except: + except Exception: user_msg_str = message["content"] else: # Otherwise just dump the full json try: user_msg_json = json_loads(message["content"]) user_msg_str = json_dumps(user_msg_json, indent=self.json_indent) - except: + except Exception: user_msg_str = message["content"] prompt += user_msg_str @@ -185,7 +185,7 @@ class ChatMLInnerMonologueWrapper(LLMChatCompletionWrapper): # indent the function replies function_return_dict = json_loads(message["content"]) function_return_str = json_dumps(function_return_dict, indent=0) - except: + except Exception: function_return_str = message["content"] prompt += function_return_str @@ -218,7 +218,7 @@ class ChatMLInnerMonologueWrapper(LLMChatCompletionWrapper): msg_json = json_loads(message["content"]) if msg_json["type"] != "user_message": role_str = "system" - except: + except Exception: pass prompt += f"\n<|im_start|>{role_str}\n{msg_str.strip()}<|im_end|>" diff --git a/letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py b/letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py index 9f53fa83..842eab52 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +++ b/letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py @@ -141,9 +141,9 @@ class ConfigurableJSONWrapper(LLMChatCompletionWrapper): # need to add the function call if there was one inner_thoughts = message["content"] - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{self._compile_function_call(message['function_call'], inner_thoughts=inner_thoughts)}" - elif "tool_calls" in message and message["tool_calls"]: + elif message.get("tool_calls"): for tool_call in message["tool_calls"]: prompt += f"\n{self._compile_function_call(tool_call['function'], inner_thoughts=inner_thoughts)}" else: @@ -161,14 +161,14 @@ class ConfigurableJSONWrapper(LLMChatCompletionWrapper): try: user_msg_json = json_loads(message["content"]) user_msg_str = user_msg_json["message"] - except: + except Exception: user_msg_str = message["content"] else: # Otherwise just dump the full json try: user_msg_json = json_loads(message["content"]) user_msg_str = json_dumps(user_msg_json, indent=self.json_indent) - except: + except Exception: user_msg_str = message["content"] prompt += user_msg_str @@ -183,7 +183,7 @@ class ConfigurableJSONWrapper(LLMChatCompletionWrapper): # indent the function replies function_return_dict = json_loads(message["content"]) function_return_str = json_dumps(function_return_dict, indent=0) - except: + except Exception: function_return_str = message["content"] prompt += function_return_str diff --git a/letta/local_llm/llm_chat_completion_wrappers/dolphin.py b/letta/local_llm/llm_chat_completion_wrappers/dolphin.py index e393d9b1..ff7d05f3 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/dolphin.py +++ b/letta/local_llm/llm_chat_completion_wrappers/dolphin.py @@ -158,7 +158,7 @@ class Dolphin21MistralWrapper(LLMChatCompletionWrapper): content_simple = content_json["message"] prompt += f"\n{IM_START_TOKEN}user\n{content_simple}{IM_END_TOKEN}" # prompt += f"\nUSER: {content_simple}" - except: + except Exception: prompt += f"\n{IM_START_TOKEN}user\n{message['content']}{IM_END_TOKEN}" # prompt += f"\nUSER: {message['content']}" elif message["role"] == "assistant": @@ -167,7 +167,7 @@ class Dolphin21MistralWrapper(LLMChatCompletionWrapper): prompt += f"\n{message['content']}" # prompt += f"\nASSISTANT: {message['content']}" # need to add the function call if there was one - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{create_function_call(message['function_call'])}" prompt += f"{IM_END_TOKEN}" elif message["role"] in ["function", "tool"]: diff --git a/letta/local_llm/llm_chat_completion_wrappers/llama3.py b/letta/local_llm/llm_chat_completion_wrappers/llama3.py index 12153209..49506d6c 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/llama3.py +++ b/letta/local_llm/llm_chat_completion_wrappers/llama3.py @@ -142,9 +142,9 @@ class LLaMA3InnerMonologueWrapper(LLMChatCompletionWrapper): # need to add the function call if there was one inner_thoughts = message["content"] - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{self._compile_function_call(message['function_call'], inner_thoughts=inner_thoughts)}" - elif "tool_calls" in message and message["tool_calls"]: + elif message.get("tool_calls"): for tool_call in message["tool_calls"]: prompt += f"\n{self._compile_function_call(tool_call['function'], inner_thoughts=inner_thoughts)}" else: @@ -162,7 +162,7 @@ class LLaMA3InnerMonologueWrapper(LLMChatCompletionWrapper): try: user_msg_json = json_loads(message["content"]) user_msg_str = user_msg_json["message"] - except: + except Exception: user_msg_str = message["content"] else: # Otherwise just dump the full json @@ -172,7 +172,7 @@ class LLaMA3InnerMonologueWrapper(LLMChatCompletionWrapper): user_msg_json, indent=self.json_indent, ) - except: + except Exception: user_msg_str = message["content"] prompt += user_msg_str @@ -190,7 +190,7 @@ class LLaMA3InnerMonologueWrapper(LLMChatCompletionWrapper): function_return_dict, indent=self.json_indent, ) - except: + except Exception: function_return_str = message["content"] prompt += function_return_str @@ -223,7 +223,7 @@ class LLaMA3InnerMonologueWrapper(LLMChatCompletionWrapper): msg_json = json_loads(message["content"]) if msg_json["type"] != "user_message": role_str = "system" - except: + except Exception: pass prompt += f"\n<|start_header_id|>{role_str}<|end_header_id|>\n\n{msg_str.strip()}<|eot_id|>" diff --git a/letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py b/letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py index d20bd2d3..ca77c9ea 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +++ b/letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py @@ -101,14 +101,14 @@ class SimpleSummaryWrapper(LLMChatCompletionWrapper): content_json = json_loads(message["content"]) content_simple = content_json["message"] prompt += f"\nUSER: {content_simple}" - except: + except Exception: prompt += f"\nUSER: {message['content']}" elif message["role"] == "assistant": prompt += f"\nASSISTANT: {message['content']}" # need to add the function call if there was one - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{create_function_call(message['function_call'])}" - elif "tool_calls" in message and message["tool_calls"]: + elif message.get("tool_calls"): prompt += f"\n{create_function_call(message['tool_calls'][0]['function'])}" elif message["role"] in ["function", "tool"]: # TODO find a good way to add this diff --git a/letta/local_llm/llm_chat_completion_wrappers/zephyr.py b/letta/local_llm/llm_chat_completion_wrappers/zephyr.py index 8ee733aa..186336ee 100644 --- a/letta/local_llm/llm_chat_completion_wrappers/zephyr.py +++ b/letta/local_llm/llm_chat_completion_wrappers/zephyr.py @@ -88,7 +88,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper): content_simple = content_json["message"] prompt += f"\n<|user|>\n{content_simple}{IM_END_TOKEN}" # prompt += f"\nUSER: {content_simple}" - except: + except Exception: prompt += f"\n<|user|>\n{message['content']}{IM_END_TOKEN}" # prompt += f"\nUSER: {message['content']}" elif message["role"] == "assistant": @@ -97,7 +97,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper): prompt += f"\n{message['content']}" # prompt += f"\nASSISTANT: {message['content']}" # need to add the function call if there was one - if "function_call" in message and message["function_call"]: + if message.get("function_call"): prompt += f"\n{create_function_call(message['function_call'])}" prompt += f"{IM_END_TOKEN}" elif message["role"] in ["function", "tool"]: @@ -256,7 +256,7 @@ class ZephyrMistralInnerMonologueWrapper(ZephyrMistralWrapper): content_json = json_loads(message["content"]) content_simple = content_json["message"] prompt += f"\n<|user|>\n{content_simple}{IM_END_TOKEN}" - except: + except Exception: prompt += f"\n<|user|>\n{message['content']}{IM_END_TOKEN}" elif message["role"] == "assistant": prompt += "\n<|assistant|>" diff --git a/letta/local_llm/lmstudio/api.py b/letta/local_llm/lmstudio/api.py index dd0debee..155f5b26 100644 --- a/letta/local_llm/lmstudio/api.py +++ b/letta/local_llm/lmstudio/api.py @@ -3,7 +3,6 @@ from urllib.parse import urljoin from letta.local_llm.settings.settings import get_completions_settings from letta.local_llm.utils import post_json_auth_request -from letta.utils import count_tokens LMSTUDIO_API_CHAT_SUFFIX = "/v1/chat/completions" LMSTUDIO_API_COMPLETIONS_SUFFIX = "/v1/completions" @@ -80,7 +79,8 @@ def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_windo """Based on the example for using LM Studio as a backend from https://github.com/lmstudio-ai/examples/tree/main/Hello%2C%20world%20-%20OpenAI%20python%20client""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/local_llm/ollama/api.py b/letta/local_llm/ollama/api.py index 69926a43..4c6508d8 100644 --- a/letta/local_llm/ollama/api.py +++ b/letta/local_llm/ollama/api.py @@ -3,7 +3,6 @@ from urllib.parse import urljoin from letta.errors import LocalLLMError from letta.local_llm.settings.settings import get_completions_settings from letta.local_llm.utils import post_json_auth_request -from letta.utils import count_tokens OLLAMA_API_SUFFIX = "/api/generate" @@ -12,7 +11,8 @@ def get_ollama_completion(endpoint, auth_type, auth_key, model, prompt, context_ """See https://github.com/jmorganca/ollama/blob/main/docs/api.md for instructions on how to run the LLM web server""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/local_llm/vllm/api.py b/letta/local_llm/vllm/api.py index dde863c8..245a176d 100644 --- a/letta/local_llm/vllm/api.py +++ b/letta/local_llm/vllm/api.py @@ -1,7 +1,7 @@ from urllib.parse import urljoin from letta.local_llm.settings.settings import get_completions_settings -from letta.local_llm.utils import count_tokens, post_json_auth_request +from letta.local_llm.utils import post_json_auth_request WEBUI_API_SUFFIX = "/completions" @@ -10,7 +10,8 @@ def get_vllm_completion(endpoint, auth_type, auth_key, model, prompt, context_wi """https://github.com/vllm-project/vllm/blob/main/examples/api_client.py""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/local_llm/webui/api.py b/letta/local_llm/webui/api.py index 7c4a0967..46323dfd 100644 --- a/letta/local_llm/webui/api.py +++ b/letta/local_llm/webui/api.py @@ -1,7 +1,7 @@ from urllib.parse import urljoin from letta.local_llm.settings.settings import get_completions_settings -from letta.local_llm.utils import count_tokens, post_json_auth_request +from letta.local_llm.utils import post_json_auth_request WEBUI_API_SUFFIX = "/v1/completions" @@ -10,7 +10,8 @@ def get_webui_completion(endpoint, auth_type, auth_key, prompt, context_window, """Compatibility for the new OpenAI API: https://github.com/oobabooga/text-generation-webui/wiki/12-%E2%80%90-OpenAI-API#examples""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/local_llm/webui/legacy_api.py b/letta/local_llm/webui/legacy_api.py index 01403c1f..c8337180 100644 --- a/letta/local_llm/webui/legacy_api.py +++ b/letta/local_llm/webui/legacy_api.py @@ -1,7 +1,7 @@ from urllib.parse import urljoin from letta.local_llm.settings.settings import get_completions_settings -from letta.local_llm.utils import count_tokens, post_json_auth_request +from letta.local_llm.utils import post_json_auth_request WEBUI_API_SUFFIX = "/api/v1/generate" @@ -10,7 +10,8 @@ def get_webui_completion(endpoint, auth_type, auth_key, prompt, context_window, """See https://github.com/oobabooga/text-generation-webui for instructions on how to run the LLM web server""" from letta.utils import printd - prompt_tokens = count_tokens(prompt) + # Approximate token count: bytes / 4 + prompt_tokens = len(prompt.encode("utf-8")) // 4 if prompt_tokens > context_window: raise Exception(f"Request exceeds maximum context length ({prompt_tokens} > {context_window} tokens)") diff --git a/letta/orm/agent.py b/letta/orm/agent.py index 86540196..b6d32720 100644 --- a/letta/orm/agent.py +++ b/letta/orm/agent.py @@ -32,9 +32,12 @@ if TYPE_CHECKING: from letta.orm.archives_agents import ArchivesAgents from letta.orm.conversation import Conversation from letta.orm.files_agents import FileAgent + from letta.orm.group import Group from letta.orm.identity import Identity + from letta.orm.llm_batch_items import LLMBatchItem from letta.orm.organization import Organization from letta.orm.run import Run + from letta.orm.sandbox_config import AgentEnvironmentVariable from letta.orm.source import Source from letta.orm.tool import Tool @@ -122,7 +125,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin # relationships organization: Mapped["Organization"] = relationship("Organization", back_populates="agents", lazy="raise") - tool_exec_environment_variables: Mapped[List["AgentEnvironmentVariable"]] = relationship( # noqa: F821 + tool_exec_environment_variables: Mapped[List["AgentEnvironmentVariable"]] = relationship( "AgentEnvironmentVariable", back_populates="agent", cascade="all, delete-orphan", @@ -160,14 +163,14 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin back_populates="agents", passive_deletes=True, ) - groups: Mapped[List["Group"]] = relationship( # noqa: F821 + groups: Mapped[List["Group"]] = relationship( "Group", secondary="groups_agents", lazy="raise", back_populates="agents", passive_deletes=True, ) - multi_agent_group: Mapped["Group"] = relationship( # noqa: F821 + multi_agent_group: Mapped["Group"] = relationship( "Group", lazy="selectin", viewonly=True, @@ -175,7 +178,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin foreign_keys="[Group.manager_agent_id]", uselist=False, ) - batch_items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="agent", lazy="raise") # noqa: F821 + batch_items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="agent", lazy="raise") file_agents: Mapped[List["FileAgent"]] = relationship( "FileAgent", back_populates="agent", diff --git a/letta/orm/agents_tags.py b/letta/orm/agents_tags.py index 0507a10f..a61a59b2 100644 --- a/letta/orm/agents_tags.py +++ b/letta/orm/agents_tags.py @@ -1,3 +1,8 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from letta.orm.agent import Agent + from sqlalchemy import ForeignKey, Index, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -21,4 +26,4 @@ class AgentsTags(Base): tag: Mapped[str] = mapped_column(String, doc="The name of the tag associated with the agent.", primary_key=True) # Relationships - agent: Mapped["Agent"] = relationship("Agent", back_populates="tags") # noqa: F821 + agent: Mapped["Agent"] = relationship("Agent", back_populates="tags") diff --git a/letta/orm/archives_agents.py b/letta/orm/archives_agents.py index c39e98df..85472408 100644 --- a/letta/orm/archives_agents.py +++ b/letta/orm/archives_agents.py @@ -1,4 +1,9 @@ from datetime import datetime +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from letta.orm.agent import Agent + from letta.orm.archive import Archive from sqlalchemy import Boolean, DateTime, ForeignKey, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -23,5 +28,5 @@ class ArchivesAgents(Base): is_owner: Mapped[bool] = mapped_column(Boolean, default=False, doc="Whether this agent created/owns the archive") # relationships - agent: Mapped["Agent"] = relationship("Agent", back_populates="archives_agents") # noqa: F821 - archive: Mapped["Archive"] = relationship("Archive", back_populates="archives_agents") # noqa: F821 + agent: Mapped["Agent"] = relationship("Agent", back_populates="archives_agents") + archive: Mapped["Archive"] = relationship("Archive", back_populates="archives_agents") diff --git a/letta/orm/base.py b/letta/orm/base.py index 8145dfcb..c9a056d0 100644 --- a/letta/orm/base.py +++ b/letta/orm/base.py @@ -78,7 +78,7 @@ class CommonSqlalchemyMetaMixins(Base): setattr(self, full_prop, None) return # Safety check - prefix, id_ = value.split("-", 1) + prefix, _id = value.split("-", 1) assert prefix == "user", f"{prefix} is not a valid id prefix for a user id" # Set the full value diff --git a/letta/orm/block.py b/letta/orm/block.py index a9faca50..7a73ee47 100644 --- a/letta/orm/block.py +++ b/letta/orm/block.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Optional, Type +from typing import TYPE_CHECKING, ClassVar, List, Optional, Type from sqlalchemy import JSON, BigInteger, ForeignKey, Index, Integer, String, UniqueConstraint, event from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship @@ -11,7 +11,9 @@ from letta.schemas.block import Block as PydanticBlock, Human, Persona if TYPE_CHECKING: from letta.orm import Organization + from letta.orm.agent import Agent from letta.orm.blocks_tags import BlocksTags + from letta.orm.group import Group from letta.orm.identity import Identity @@ -56,11 +58,11 @@ class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin, TemplateEntityMixin ) # NOTE: This takes advantage of built-in optimistic locking functionality by SqlAlchemy # https://docs.sqlalchemy.org/en/20/orm/versioning.html - __mapper_args__ = {"version_id_col": version} + __mapper_args__: ClassVar[dict] = {"version_id_col": version} # relationships organization: Mapped[Optional["Organization"]] = relationship("Organization", lazy="raise") - agents: Mapped[List["Agent"]] = relationship( # noqa: F821 + agents: Mapped[List["Agent"]] = relationship( "Agent", secondary="blocks_agents", lazy="raise", @@ -75,7 +77,7 @@ class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin, TemplateEntityMixin back_populates="blocks", passive_deletes=True, ) - groups: Mapped[List["Group"]] = relationship( # noqa: F821 + groups: Mapped[List["Group"]] = relationship( "Group", secondary="groups_blocks", lazy="raise", diff --git a/letta/orm/blocks_tags.py b/letta/orm/blocks_tags.py index 90f79678..0f7969dc 100644 --- a/letta/orm/blocks_tags.py +++ b/letta/orm/blocks_tags.py @@ -1,5 +1,8 @@ from datetime import datetime -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from letta.orm.block import Block from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -34,4 +37,4 @@ class BlocksTags(Base): _last_updated_by_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) # Relationships - block: Mapped["Block"] = relationship("Block", back_populates="tags") # noqa: F821 + block: Mapped["Block"] = relationship("Block", back_populates="tags") diff --git a/letta/orm/files_agents.py b/letta/orm/files_agents.py index 9757cd2e..f9486cbe 100644 --- a/letta/orm/files_agents.py +++ b/letta/orm/files_agents.py @@ -12,7 +12,7 @@ from letta.schemas.file import FileAgent as PydanticFileAgent from letta.utils import truncate_file_visible_content if TYPE_CHECKING: - pass + from letta.orm.agent import Agent class FileAgent(SqlalchemyBase, OrganizationMixin): @@ -85,7 +85,7 @@ class FileAgent(SqlalchemyBase, OrganizationMixin): ) # relationships - agent: Mapped["Agent"] = relationship( # noqa: F821 + agent: Mapped["Agent"] = relationship( "Agent", back_populates="file_agents", lazy="selectin", diff --git a/letta/orm/group.py b/letta/orm/group.py index 28653882..7fe3298a 100644 --- a/letta/orm/group.py +++ b/letta/orm/group.py @@ -1,5 +1,10 @@ import uuid -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from letta.orm.agent import Agent + from letta.orm.block import Block + from letta.orm.organization import Organization from sqlalchemy import JSON, ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -27,12 +32,12 @@ class Group(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateMixin): hidden: Mapped[Optional[bool]] = mapped_column(nullable=True, doc="If set to True, the group will be hidden.") # relationships - organization: Mapped["Organization"] = relationship("Organization", back_populates="groups") # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", back_populates="groups") agent_ids: Mapped[List[str]] = mapped_column(JSON, nullable=False, doc="Ordered list of agent IDs in this group") - agents: Mapped[List["Agent"]] = relationship( # noqa: F821 + agents: Mapped[List["Agent"]] = relationship( "Agent", secondary="groups_agents", lazy="selectin", passive_deletes=True, back_populates="groups" ) - shared_blocks: Mapped[List["Block"]] = relationship( # noqa: F821 + shared_blocks: Mapped[List["Block"]] = relationship( "Block", secondary="groups_blocks", lazy="selectin", passive_deletes=True, back_populates="groups" ) - manager_agent: Mapped["Agent"] = relationship("Agent", lazy="joined", back_populates="multi_agent_group") # noqa: F821 + manager_agent: Mapped["Agent"] = relationship("Agent", lazy="joined", back_populates="multi_agent_group") diff --git a/letta/orm/identity.py b/letta/orm/identity.py index b0058a69..5badaf81 100644 --- a/letta/orm/identity.py +++ b/letta/orm/identity.py @@ -1,5 +1,10 @@ import uuid -from typing import List +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from letta.orm.agent import Agent + from letta.orm.block import Block + from letta.orm.organization import Organization from sqlalchemy import String, UniqueConstraint from sqlalchemy.dialects.postgresql import JSON @@ -36,11 +41,11 @@ class Identity(SqlalchemyBase, OrganizationMixin, ProjectMixin): ) # relationships - organization: Mapped["Organization"] = relationship("Organization", back_populates="identities") # noqa: F821 - agents: Mapped[List["Agent"]] = relationship( # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", back_populates="identities") + agents: Mapped[List["Agent"]] = relationship( "Agent", secondary="identities_agents", lazy="selectin", passive_deletes=True, back_populates="identities" ) - blocks: Mapped[List["Block"]] = relationship( # noqa: F821 + blocks: Mapped[List["Block"]] = relationship( "Block", secondary="identities_blocks", lazy="selectin", passive_deletes=True, back_populates="identities" ) diff --git a/letta/orm/llm_batch_items.py b/letta/orm/llm_batch_items.py index 71d3c255..e027af25 100644 --- a/letta/orm/llm_batch_items.py +++ b/letta/orm/llm_batch_items.py @@ -1,5 +1,10 @@ import uuid -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union + +if TYPE_CHECKING: + from letta.orm.agent import Agent + from letta.orm.llm_batch_job import LLMBatchJob + from letta.orm.organization import Organization from anthropic.types.beta.messages import BetaMessageBatchIndividualResponse from sqlalchemy import ForeignKey, Index, String @@ -49,6 +54,6 @@ class LLMBatchItem(SqlalchemyBase, OrganizationMixin, AgentMixin): ) # relationships - organization: Mapped["Organization"] = relationship("Organization", back_populates="llm_batch_items") # noqa: F821 - batch: Mapped["LLMBatchJob"] = relationship("LLMBatchJob", back_populates="items", lazy="selectin") # noqa: F821 - agent: Mapped["Agent"] = relationship("Agent", back_populates="batch_items", lazy="selectin") # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", back_populates="llm_batch_items") + batch: Mapped["LLMBatchJob"] = relationship("LLMBatchJob", back_populates="items", lazy="selectin") + agent: Mapped["Agent"] = relationship("Agent", back_populates="batch_items", lazy="selectin") diff --git a/letta/orm/llm_batch_job.py b/letta/orm/llm_batch_job.py index cf67e4d1..a3b09e7b 100644 --- a/letta/orm/llm_batch_job.py +++ b/letta/orm/llm_batch_job.py @@ -1,6 +1,10 @@ import uuid from datetime import datetime -from typing import List, Optional, Union +from typing import TYPE_CHECKING, List, Optional, Union + +if TYPE_CHECKING: + from letta.orm.llm_batch_items import LLMBatchItem + from letta.orm.organization import Organization from anthropic.types.beta.messages import BetaMessageBatch from sqlalchemy import DateTime, ForeignKey, Index, String @@ -47,5 +51,5 @@ class LLMBatchJob(SqlalchemyBase, OrganizationMixin): String, ForeignKey("jobs.id", ondelete="CASCADE"), nullable=False, doc="ID of the Letta batch job" ) - organization: Mapped["Organization"] = relationship("Organization", back_populates="llm_batch_jobs") # noqa: F821 - items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="batch", lazy="selectin") # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", back_populates="llm_batch_jobs") + items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="batch", lazy="selectin") diff --git a/letta/orm/message.py b/letta/orm/message.py index f3c8bb33..10b6b562 100644 --- a/letta/orm/message.py +++ b/letta/orm/message.py @@ -1,4 +1,10 @@ -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from letta.orm.job import Job + from letta.orm.organization import Organization + from letta.orm.run import Run + from letta.orm.step import Step from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall from sqlalchemy import BigInteger, FetchedValue, ForeignKey, Index, event, text @@ -83,12 +89,12 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin): ) # Relationships - organization: Mapped["Organization"] = relationship("Organization", back_populates="messages", lazy="raise") # noqa: F821 - step: Mapped["Step"] = relationship("Step", back_populates="messages", lazy="selectin") # noqa: F821 - run: Mapped["Run"] = relationship("Run", back_populates="messages", lazy="selectin") # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", back_populates="messages", lazy="raise") + step: Mapped["Step"] = relationship("Step", back_populates="messages", lazy="selectin") + run: Mapped["Run"] = relationship("Run", back_populates="messages", lazy="selectin") @property - def job(self) -> Optional["Job"]: # noqa: F821 + def job(self) -> Optional["Job"]: """Get the job associated with this message, if any.""" return self.job_message.job if self.job_message else None diff --git a/letta/orm/passage.py b/letta/orm/passage.py index 457a8ec6..fb59aff5 100644 --- a/letta/orm/passage.py +++ b/letta/orm/passage.py @@ -15,6 +15,7 @@ config = LettaConfig() if TYPE_CHECKING: from letta.orm.organization import Organization + from letta.orm.passage_tag import PassageTag class BasePassage(SqlalchemyBase, OrganizationMixin): @@ -78,7 +79,7 @@ class ArchivalPassage(BasePassage, ArchiveMixin): __tablename__ = "archival_passages" # junction table for efficient tag queries (complements json column above) - passage_tags: Mapped[List["PassageTag"]] = relationship( # noqa: F821 + passage_tags: Mapped[List["PassageTag"]] = relationship( "PassageTag", back_populates="passage", cascade="all, delete-orphan", lazy="noload" ) diff --git a/letta/orm/provider_trace.py b/letta/orm/provider_trace.py index 9a3875f3..2a4e4bd2 100644 --- a/letta/orm/provider_trace.py +++ b/letta/orm/provider_trace.py @@ -1,5 +1,8 @@ import uuid -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from letta.orm.organization import Organization from sqlalchemy import JSON, Index, String from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -43,4 +46,4 @@ class ProviderTrace(SqlalchemyBase, OrganizationMixin): ) # Relationships - organization: Mapped["Organization"] = relationship("Organization", lazy="selectin") # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", lazy="selectin") diff --git a/letta/orm/provider_trace_metadata.py b/letta/orm/provider_trace_metadata.py index 55d4b0ab..1d632a1e 100644 --- a/letta/orm/provider_trace_metadata.py +++ b/letta/orm/provider_trace_metadata.py @@ -1,6 +1,9 @@ import uuid from datetime import datetime -from typing import Optional +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from letta.orm.organization import Organization from sqlalchemy import JSON, DateTime, Index, String, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -42,4 +45,4 @@ class ProviderTraceMetadata(SqlalchemyBase, OrganizationMixin): user_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="ID of the user who initiated the request") # Relationships - organization: Mapped["Organization"] = relationship("Organization", lazy="selectin") # noqa: F821 + organization: Mapped["Organization"] = relationship("Organization", lazy="selectin") diff --git a/letta/orm/sqlalchemy_base.py b/letta/orm/sqlalchemy_base.py index ba85cbf2..314a565f 100644 --- a/letta/orm/sqlalchemy_base.py +++ b/letta/orm/sqlalchemy_base.py @@ -30,6 +30,9 @@ from letta.settings import DatabaseChoice if TYPE_CHECKING: from pydantic import BaseModel + from sqlalchemy import Select + + from letta.schemas.user import User logger = get_logger(__name__) @@ -122,7 +125,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): query_text: Optional[str] = None, query_embedding: Optional[List[float]] = None, ascending: bool = True, - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, access: Optional[List[Literal["read", "write", "admin"]]] = ["read"], access_type: AccessType = AccessType.ORGANIZATION, join_model: Optional[Base] = None, @@ -222,7 +225,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): query_text: Optional[str] = None, query_embedding: Optional[List[float]] = None, ascending: bool = True, - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, access: Optional[List[Literal["read", "write", "admin"]]] = ["read"], access_type: AccessType = AccessType.ORGANIZATION, join_model: Optional[Base] = None, @@ -415,7 +418,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls, db_session: "AsyncSession", identifier: Optional[str] = None, - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, access: Optional[List[Literal["read", "write", "admin"]]] = ["read"], access_type: AccessType = AccessType.ORGANIZATION, check_is_deleted: bool = False, @@ -451,7 +454,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls, db_session: "AsyncSession", identifiers: List[str] = [], - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, access: Optional[List[Literal["read", "write", "admin"]]] = ["read"], access_type: AccessType = AccessType.ORGANIZATION, check_is_deleted: bool = False, @@ -471,7 +474,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): def _read_multiple_preprocess( cls, identifiers: List[str], - actor: Optional["User"], # noqa: F821 + actor: Optional["User"], access: Optional[List[Literal["read", "write", "admin"]]], access_type: AccessType, check_is_deleted: bool, @@ -543,7 +546,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): async def create_async( self, db_session: "AsyncSession", - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, no_commit: bool = False, no_refresh: bool = False, ignore_conflicts: bool = False, @@ -599,7 +602,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls, items: List["SqlalchemyBase"], db_session: "AsyncSession", - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, no_commit: bool = False, no_refresh: bool = False, ) -> List["SqlalchemyBase"]: @@ -654,7 +657,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls._handle_dbapi_error(e) @handle_db_timeout - async def delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> "SqlalchemyBase": # noqa: F821 + async def delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> "SqlalchemyBase": """Soft delete a record asynchronously (mark as deleted).""" logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor} (async)") @@ -665,7 +668,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): return await self.update_async(db_session) @handle_db_timeout - async def hard_delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> None: # noqa: F821 + async def hard_delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> None: """Permanently removes the record from the database asynchronously.""" obj_id = self.id obj_class = self.__class__.__name__ @@ -694,7 +697,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls, db_session: "AsyncSession", identifiers: List[str], - actor: Optional["User"], # noqa: F821 + actor: Optional["User"], access: Optional[List[Literal["read", "write", "admin"]]] = ["write"], access_type: AccessType = AccessType.ORGANIZATION, ) -> None: @@ -731,7 +734,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): async def update_async( self, db_session: "AsyncSession", - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, no_commit: bool = False, no_refresh: bool = False, ) -> "SqlalchemyBase": @@ -778,7 +781,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls, *, db_session: "Session", - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, access: Optional[List[Literal["read", "write", "admin"]]] = ["read"], access_type: AccessType = AccessType.ORGANIZATION, check_is_deleted: bool = False, @@ -818,7 +821,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): cls, *, db_session: "AsyncSession", - actor: Optional["User"] = None, # noqa: F821 + actor: Optional["User"] = None, access: Optional[List[Literal["read", "write", "admin"]]] = ["read"], access_type: AccessType = AccessType.ORGANIZATION, check_is_deleted: bool = False, @@ -854,11 +857,11 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base): @classmethod def apply_access_predicate( cls, - query: "Select", # noqa: F821 - actor: "User", # noqa: F821 + query: "Select", + actor: "User", access: List[Literal["read", "write", "admin"]], access_type: AccessType = AccessType.ORGANIZATION, - ) -> "Select": # noqa: F821 + ) -> "Select": """applies a WHERE clause restricting results to the given actor and access level Args: query: The initial sqlalchemy select statement diff --git a/letta/otel/tracing.py b/letta/otel/tracing.py index 78144911..f0f5f490 100644 --- a/letta/otel/tracing.py +++ b/letta/otel/tracing.py @@ -339,7 +339,7 @@ def trace_method(func): try: # Test if str() works (some objects have broken __str__) try: - test_str = str(value) + str(value) # If str() works and is reasonable, use repr str_value = repr(value) except Exception: diff --git a/letta/schemas/letta_message.py b/letta/schemas/letta_message.py index 6b25a0c6..1482e13c 100644 --- a/letta/schemas/letta_message.py +++ b/letta/schemas/letta_message.py @@ -1,7 +1,7 @@ import json from datetime import datetime, timezone from enum import Enum -from typing import Annotated, List, Literal, Optional, Union +from typing import Annotated, ClassVar, List, Literal, Optional, Union from pydantic import BaseModel, Field, field_serializer, field_validator @@ -246,7 +246,7 @@ class ToolCallMessage(LettaMessage): return data class Config: - json_encoders = { + json_encoders: ClassVar[dict] = { ToolCallDelta: lambda v: v.model_dump(exclude_none=True), ToolCall: lambda v: v.model_dump(exclude_none=True), } diff --git a/letta/schemas/message.py b/letta/schemas/message.py index f9c63829..6368db2f 100644 --- a/letta/schemas/message.py +++ b/letta/schemas/message.py @@ -1150,7 +1150,7 @@ class Message(BaseMessage): tool_returns = [ToolReturn(**tr) for tr in openai_message_dict["tool_returns"]] # TODO(caren) bad assumption here that "reasoning_content" always comes before "redacted_reasoning_content" - if "reasoning_content" in openai_message_dict and openai_message_dict["reasoning_content"]: + if openai_message_dict.get("reasoning_content"): content.append( ReasoningContent( reasoning=openai_message_dict["reasoning_content"], @@ -1162,13 +1162,13 @@ class Message(BaseMessage): ), ), ) - if "redacted_reasoning_content" in openai_message_dict and openai_message_dict["redacted_reasoning_content"]: + if openai_message_dict.get("redacted_reasoning_content"): content.append( RedactedReasoningContent( data=str(openai_message_dict["redacted_reasoning_content"]), ), ) - if "omitted_reasoning_content" in openai_message_dict and openai_message_dict["omitted_reasoning_content"]: + if openai_message_dict.get("omitted_reasoning_content"): content.append( OmittedReasoningContent(), ) @@ -2237,7 +2237,7 @@ class Message(BaseMessage): try: # NOTE: Google AI wants actual JSON objects, not strings function_args = parse_json(function_args) - except: + except Exception: raise UserWarning(f"Failed to parse JSON function args: {function_args}") function_args = {"args": function_args} @@ -2327,7 +2327,7 @@ class Message(BaseMessage): try: function_response = parse_json(text_content) - except: + except Exception: function_response = {"function_response": text_content} parts.append( @@ -2360,7 +2360,7 @@ class Message(BaseMessage): # NOTE: Google AI API wants the function response as JSON only, no string try: function_response = parse_json(legacy_content) - except: + except Exception: function_response = {"function_response": legacy_content} google_ai_message = { diff --git a/letta/schemas/providers/__init__.py b/letta/schemas/providers/__init__.py index 2790ba7e..40e0e333 100644 --- a/letta/schemas/providers/__init__.py +++ b/letta/schemas/providers/__init__.py @@ -24,13 +24,6 @@ from .xai import XAIProvider from .zai import ZAIProvider __all__ = [ - # Base classes - "Provider", - "ProviderBase", - "ProviderCreate", - "ProviderUpdate", - "ProviderCheck", - # Provider implementations "AnthropicProvider", "AzureProvider", "BedrockProvider", @@ -40,16 +33,21 @@ __all__ = [ "GoogleAIProvider", "GoogleVertexProvider", "GroqProvider", - "LettaProvider", "LMStudioOpenAIProvider", + "LettaProvider", "MiniMaxProvider", "MistralProvider", "OllamaProvider", "OpenAIProvider", - "TogetherProvider", - "VLLMProvider", # Replaces ChatCompletions and Completions + "OpenRouterProvider", + "Provider", + "ProviderBase", + "ProviderCheck", + "ProviderCreate", + "ProviderUpdate", "SGLangProvider", + "TogetherProvider", + "VLLMProvider", "XAIProvider", "ZAIProvider", - "OpenRouterProvider", ] diff --git a/letta/schemas/providers/lmstudio.py b/letta/schemas/providers/lmstudio.py index c656f188..12079b9c 100644 --- a/letta/schemas/providers/lmstudio.py +++ b/letta/schemas/providers/lmstudio.py @@ -92,7 +92,7 @@ class LMStudioOpenAIProvider(OpenAIProvider): check = self._do_model_checks_for_name_and_context_size(model, length_key="max_context_length") if check is None: continue - model_name, context_window_size = check + model_name, _context_window_size = check configs.append( EmbeddingConfig( diff --git a/letta/schemas/providers/openrouter.py b/letta/schemas/providers/openrouter.py index f349a3c0..10136044 100644 --- a/letta/schemas/providers/openrouter.py +++ b/letta/schemas/providers/openrouter.py @@ -93,7 +93,7 @@ class OpenRouterProvider(OpenAIProvider): model_name = model["id"] # OpenRouter returns context_length in the model listing - if "context_length" in model and model["context_length"]: + if model.get("context_length"): context_window_size = model["context_length"] else: context_window_size = self.get_model_context_window_size(model_name) diff --git a/letta/schemas/tool.py b/letta/schemas/tool.py index 3f8726d2..9f7f6853 100644 --- a/letta/schemas/tool.py +++ b/letta/schemas/tool.py @@ -158,7 +158,7 @@ class ToolCreate(LettaBase): description = mcp_tool.description source_type = "python" tags = [f"{MCP_TOOL_TAG_NAME_PREFIX}:{mcp_server_name}"] - wrapper_func_name, wrapper_function_str = generate_mcp_tool_wrapper(mcp_tool.name) + _wrapper_func_name, wrapper_function_str = generate_mcp_tool_wrapper(mcp_tool.name) return cls( description=description, diff --git a/letta/schemas/usage.py b/letta/schemas/usage.py index e67ab13d..00d59bc4 100644 --- a/letta/schemas/usage.py +++ b/letta/schemas/usage.py @@ -3,7 +3,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Uni from pydantic import BaseModel, Field if TYPE_CHECKING: + from letta.schemas.enums import ProviderType from letta.schemas.openai.chat_completion_response import ( + UsageStatistics, UsageStatisticsCompletionTokenDetails, UsageStatisticsPromptTokenDetails, ) @@ -131,7 +133,7 @@ class LettaUsageStatistics(BaseModel): description="Estimate of tokens currently in the context window.", ) - def to_usage(self, provider_type: Optional["ProviderType"] = None) -> "UsageStatistics": # noqa: F821 # noqa: F821 + def to_usage(self, provider_type: Optional["ProviderType"] = None) -> "UsageStatistics": """Convert to UsageStatistics (OpenAI-compatible format). Args: diff --git a/letta/serialize_schemas/marshmallow_agent.py b/letta/serialize_schemas/marshmallow_agent.py index ddcaca38..53014659 100644 --- a/letta/serialize_schemas/marshmallow_agent.py +++ b/letta/serialize_schemas/marshmallow_agent.py @@ -112,7 +112,7 @@ class MarshmallowAgentSchema(BaseSchema): .all() ) # combine system message with step messages - msgs = [system_msg] + step_msgs if system_msg else step_msgs + msgs = [system_msg, *step_msgs] if system_msg else step_msgs else: # no user messages, just return system message msgs = [system_msg] if system_msg else [] @@ -147,7 +147,7 @@ class MarshmallowAgentSchema(BaseSchema): .all() ) # combine system message with step messages - msgs = [system_msg] + step_msgs if system_msg else step_msgs + msgs = [system_msg, *step_msgs] if system_msg else step_msgs else: # no user messages, just return system message msgs = [system_msg] if system_msg else [] @@ -231,7 +231,8 @@ class MarshmallowAgentSchema(BaseSchema): class Meta(BaseSchema.Meta): model = Agent - exclude = BaseSchema.Meta.exclude + ( + exclude = ( + *BaseSchema.Meta.exclude, "project_id", "template_id", "base_template_id", diff --git a/letta/serialize_schemas/marshmallow_agent_environment_variable.py b/letta/serialize_schemas/marshmallow_agent_environment_variable.py index 371614a8..7a4b04d2 100644 --- a/letta/serialize_schemas/marshmallow_agent_environment_variable.py +++ b/letta/serialize_schemas/marshmallow_agent_environment_variable.py @@ -18,4 +18,4 @@ class SerializedAgentEnvironmentVariableSchema(BaseSchema): class Meta(BaseSchema.Meta): model = AgentEnvironmentVariable - exclude = BaseSchema.Meta.exclude + ("agent",) + exclude = (*BaseSchema.Meta.exclude, "agent") diff --git a/letta/serialize_schemas/marshmallow_block.py b/letta/serialize_schemas/marshmallow_block.py index 082cd328..b92e91cf 100644 --- a/letta/serialize_schemas/marshmallow_block.py +++ b/letta/serialize_schemas/marshmallow_block.py @@ -34,4 +34,4 @@ class SerializedBlockSchema(BaseSchema): class Meta(BaseSchema.Meta): model = Block - exclude = BaseSchema.Meta.exclude + ("agents", "identities", "is_deleted", "groups", "organization") + exclude = (*BaseSchema.Meta.exclude, "agents", "identities", "is_deleted", "groups", "organization") diff --git a/letta/serialize_schemas/marshmallow_message.py b/letta/serialize_schemas/marshmallow_message.py index 75678bd7..5d03985d 100644 --- a/letta/serialize_schemas/marshmallow_message.py +++ b/letta/serialize_schemas/marshmallow_message.py @@ -37,4 +37,4 @@ class SerializedMessageSchema(BaseSchema): class Meta(BaseSchema.Meta): model = Message - exclude = BaseSchema.Meta.exclude + ("step", "job_message", "otid", "is_deleted", "organization") + exclude = (*BaseSchema.Meta.exclude, "step", "job_message", "otid", "is_deleted", "organization") diff --git a/letta/serialize_schemas/marshmallow_tag.py b/letta/serialize_schemas/marshmallow_tag.py index be19b90c..2b03be98 100644 --- a/letta/serialize_schemas/marshmallow_tag.py +++ b/letta/serialize_schemas/marshmallow_tag.py @@ -25,4 +25,4 @@ class SerializedAgentTagSchema(BaseSchema): class Meta(BaseSchema.Meta): model = AgentsTags - exclude = BaseSchema.Meta.exclude + ("agent",) + exclude = (*BaseSchema.Meta.exclude, "agent") diff --git a/letta/serialize_schemas/marshmallow_tool.py b/letta/serialize_schemas/marshmallow_tool.py index a6d1c91e..0d8471bf 100644 --- a/letta/serialize_schemas/marshmallow_tool.py +++ b/letta/serialize_schemas/marshmallow_tool.py @@ -34,4 +34,4 @@ class SerializedToolSchema(BaseSchema): class Meta(BaseSchema.Meta): model = Tool - exclude = BaseSchema.Meta.exclude + ("is_deleted", "organization") + exclude = (*BaseSchema.Meta.exclude, "is_deleted", "organization") diff --git a/letta/server/rest_api/app.py b/letta/server/rest_api/app.py index 6c062aa6..f287c477 100644 --- a/letta/server/rest_api/app.py +++ b/letta/server/rest_api/app.py @@ -891,7 +891,7 @@ def start_server( import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - except: + except Exception: pass if (os.getenv("LOCAL_HTTPS") == "true") or "--localhttps" in sys.argv: diff --git a/letta/server/rest_api/auth/index.py b/letta/server/rest_api/auth/index.py index 6ee6f3cc..1e982051 100644 --- a/letta/server/rest_api/auth/index.py +++ b/letta/server/rest_api/auth/index.py @@ -22,7 +22,7 @@ class AuthRequest(BaseModel): def setup_auth_router(server: SyncServer, interface: QueuingInterface, password: str) -> APIRouter: - @router.post("/auth", tags=["auth"], response_model=AuthResponse) + @router.post("/auth", tags=["auth"]) def authenticate_user(request: AuthRequest) -> AuthResponse: """ Authenticates the user and sends response with User related data. diff --git a/letta/server/rest_api/interface.py b/letta/server/rest_api/interface.py index 86ffc99e..1d290e1b 100644 --- a/letta/server/rest_api/interface.py +++ b/letta/server/rest_api/interface.py @@ -1227,7 +1227,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface): # } try: func_args = parse_json(function_call.function.arguments) - except: + except Exception: func_args = function_call.function.arguments # processed_chunk = { # "function_call": f"{function_call.function.name}({func_args})", @@ -1262,7 +1262,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface): else: try: func_args = parse_json(function_call.function.arguments) - except: + except Exception: logger.warning(f"Failed to parse function arguments: {function_call.function.arguments}") func_args = {} diff --git a/letta/server/rest_api/proxy_helpers.py b/letta/server/rest_api/proxy_helpers.py index d6e2ca4a..644132b5 100644 --- a/letta/server/rest_api/proxy_helpers.py +++ b/letta/server/rest_api/proxy_helpers.py @@ -301,7 +301,7 @@ async def inject_memory_context( # Handle both string and list system prompts if isinstance(existing_system, list): # If it's a list, prepend our context as a text block - modified_data["system"] = existing_system + [{"type": "text", "text": memory_context.rstrip()}] + modified_data["system"] = [*existing_system, {"type": "text", "text": memory_context.rstrip()}] elif existing_system: # If it's a non-empty string, prepend our context modified_data["system"] = memory_context + existing_system @@ -451,8 +451,8 @@ async def backfill_agent_project_id(server, agent, actor, project_id: str): async def get_or_create_claude_code_agent( server, actor, - project_id: str = None, - agent_id: str = None, + project_id: str | None = None, + agent_id: str | None = None, ): """ Get or create a special agent for Claude Code sessions. diff --git a/letta/server/rest_api/redis_stream_manager.py b/letta/server/rest_api/redis_stream_manager.py index f5c96168..bfa48102 100644 --- a/letta/server/rest_api/redis_stream_manager.py +++ b/letta/server/rest_api/redis_stream_manager.py @@ -276,7 +276,7 @@ async def create_background_stream_processor( maybe_stop_reason = json.loads(maybe_json_chunk) if maybe_json_chunk and maybe_json_chunk[0] == "{" else None if maybe_stop_reason and maybe_stop_reason.get("message_type") == "stop_reason": stop_reason = maybe_stop_reason.get("stop_reason") - except: + except Exception: pass # Stream ended naturally - check if we got a proper terminal @@ -313,7 +313,7 @@ async def create_background_stream_processor( # Set a default stop_reason so run status can be mapped in finally stop_reason = StopReasonType.error.value - except RunCancelledException as e: + except RunCancelledException: # Handle cancellation gracefully - don't write error chunk, cancellation event was already sent logger.info(f"Stream processing stopped due to cancellation for run {run_id}") # The cancellation event was already yielded by cancellation_aware_stream_wrapper diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index da71a349..4cc28540 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -3,9 +3,9 @@ import json from datetime import datetime from typing import Annotated, Any, Dict, List, Literal, Optional, Union +import orjson from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Query, Request, UploadFile, status from fastapi.responses import JSONResponse -from orjson import orjson from pydantic import BaseModel, ConfigDict, Field, field_validator from starlette.responses import Response, StreamingResponse @@ -879,7 +879,7 @@ async def detach_source( source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor) block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor) await server.block_manager.delete_block_async(block.id, actor) - except: + except Exception: pass return agent_state @@ -911,7 +911,7 @@ async def detach_folder_from_agent( source = await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor) block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor) await server.block_manager.delete_block_async(block.id, actor) - except: + except Exception: pass if is_1_0_sdk_version(headers): @@ -972,7 +972,7 @@ async def open_file_for_agent( visible_content = truncate_file_visible_content(visible_content, True, per_file_view_window_char_limit) # Use enforce_max_open_files_and_open for efficient LRU handling - closed_files, was_already_open, _ = await server.file_agent_manager.enforce_max_open_files_and_open( + closed_files, _was_already_open, _ = await server.file_agent_manager.enforce_max_open_files_and_open( agent_id=agent_id, file_id=file_id, file_name=file_metadata.file_name, @@ -1840,7 +1840,7 @@ async def send_message_streaming( # use the streaming service for unified stream handling streaming_service = StreamingService(server) - run, result = await streaming_service.create_agent_stream( + _run, result = await streaming_service.create_agent_stream( agent_id=agent_id, actor=actor, request=request, @@ -1921,7 +1921,6 @@ async def cancel_message( @router.post( "/{agent_id}/generate", - response_model=GenerateResponse, operation_id="generate_completion", responses={ 200: {"description": "Successful generation"}, @@ -2177,7 +2176,7 @@ async def send_message_async( try: is_message_input = request.messages[0].type == MessageCreateType.message - except: + except Exception: is_message_input = True use_lettuce = headers.experimental_params.message_async and is_message_input diff --git a/letta/server/rest_api/routers/v1/anthropic.py b/letta/server/rest_api/routers/v1/anthropic.py index 62357e75..34f7a662 100644 --- a/letta/server/rest_api/routers/v1/anthropic.py +++ b/letta/server/rest_api/routers/v1/anthropic.py @@ -21,6 +21,8 @@ from letta.server.server import SyncServer logger = get_logger(__name__) +_background_tasks: set[asyncio.Task] = set() + router = APIRouter(prefix="/anthropic", tags=["anthropic"]) ANTHROPIC_API_BASE = "https://api.anthropic.com" @@ -172,7 +174,7 @@ async def anthropic_messages_proxy( # This prevents race conditions where multiple requests persist the same message user_messages_to_persist = await check_for_duplicate_message(server, agent, actor, user_messages, PROXY_NAME) - asyncio.create_task( + task = asyncio.create_task( persist_messages_background( server=server, agent=agent, @@ -183,6 +185,8 @@ async def anthropic_messages_proxy( proxy_name=PROXY_NAME, ) ) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) return StreamingResponse( stream_response(), @@ -226,7 +230,7 @@ async def anthropic_messages_proxy( # Check for duplicate user messages before creating background task user_messages_to_persist = await check_for_duplicate_message(server, agent, actor, user_messages, PROXY_NAME) - asyncio.create_task( + task = asyncio.create_task( persist_messages_background( server=server, agent=agent, @@ -237,6 +241,8 @@ async def anthropic_messages_proxy( proxy_name=PROXY_NAME, ) ) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) except Exception as e: logger.warning(f"[{PROXY_NAME}] Failed to extract assistant response for logging: {e}") diff --git a/letta/server/rest_api/routers/v1/folders.py b/letta/server/rest_api/routers/v1/folders.py index 7449783e..67505306 100644 --- a/letta/server/rest_api/routers/v1/folders.py +++ b/letta/server/rest_api/routers/v1/folders.py @@ -331,7 +331,7 @@ async def upload_file_to_folder( return response elif duplicate_handling == DuplicateFileHandling.REPLACE: # delete the file - deleted_file = await server.file_manager.delete_file(file_id=existing_file.id, actor=actor) + await server.file_manager.delete_file(file_id=existing_file.id, actor=actor) unique_filename = original_filename if not unique_filename: diff --git a/letta/server/rest_api/routers/v1/git_http.py b/letta/server/rest_api/routers/v1/git_http.py index de2099a2..16b593fc 100644 --- a/letta/server/rest_api/routers/v1/git_http.py +++ b/letta/server/rest_api/routers/v1/git_http.py @@ -65,7 +65,8 @@ from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_le logger = get_logger(__name__) -# Routes are proxied to dulwich running on a separate port. +_background_tasks: set[asyncio.Task] = set() + router = APIRouter(prefix="/git", tags=["git"], include_in_schema=False) # Global storage for the server instance (set during app startup) @@ -718,8 +719,9 @@ async def proxy_git_http( actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) # Authorization check: ensure the actor can access this agent. await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor, include_relationships=[]) - # Fire-and-forget; do not block git client response. - asyncio.create_task(_sync_after_push(actor.id, agent_id)) + task = asyncio.create_task(_sync_after_push(actor.id, agent_id)) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) except Exception: logger.exception("Failed to trigger post-push sync (agent_id=%s)", agent_id) diff --git a/letta/server/rest_api/routers/v1/identities.py b/letta/server/rest_api/routers/v1/identities.py index d91a8b06..85ec3ef3 100644 --- a/letta/server/rest_api/routers/v1/identities.py +++ b/letta/server/rest_api/routers/v1/identities.py @@ -53,7 +53,7 @@ async def list_identities( """ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) - identities, next_cursor, has_more = await server.identity_manager.list_identities_async( + identities, _next_cursor, _has_more = await server.identity_manager.list_identities_async( name=name, project_id=project_id, identifier_key=identifier_key, diff --git a/letta/server/rest_api/routers/v1/sources.py b/letta/server/rest_api/routers/v1/sources.py index edacc64b..39f41d52 100644 --- a/letta/server/rest_api/routers/v1/sources.py +++ b/letta/server/rest_api/routers/v1/sources.py @@ -309,7 +309,7 @@ async def upload_file_to_source( return response elif duplicate_handling == DuplicateFileHandling.REPLACE: # delete the file - deleted_file = await server.file_manager.delete_file(file_id=existing_file.id, actor=actor) + await server.file_manager.delete_file(file_id=existing_file.id, actor=actor) unique_filename = original_filename if not unique_filename: diff --git a/letta/server/rest_api/routers/v1/steps.py b/letta/server/rest_api/routers/v1/steps.py index d34f1de0..b8b238b3 100644 --- a/letta/server/rest_api/routers/v1/steps.py +++ b/letta/server/rest_api/routers/v1/steps.py @@ -106,7 +106,7 @@ async def retrieve_trace_for_step( provider_trace = await server.telemetry_manager.get_provider_trace_by_step_id_async( step_id=step_id, actor=await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) ) - except: + except Exception: pass return provider_trace diff --git a/letta/server/rest_api/routers/v1/telemetry.py b/letta/server/rest_api/routers/v1/telemetry.py index e317a791..e4773e6b 100644 --- a/letta/server/rest_api/routers/v1/telemetry.py +++ b/letta/server/rest_api/routers/v1/telemetry.py @@ -27,7 +27,7 @@ async def retrieve_provider_trace( provider_trace = await server.telemetry_manager.get_provider_trace_by_step_id_async( step_id=step_id, actor=await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) ) - except: + except Exception: pass return provider_trace diff --git a/letta/server/rest_api/routers/v1/zai.py b/letta/server/rest_api/routers/v1/zai.py index b8035cee..7ac2c46a 100644 --- a/letta/server/rest_api/routers/v1/zai.py +++ b/letta/server/rest_api/routers/v1/zai.py @@ -21,6 +21,8 @@ from letta.server.server import SyncServer logger = get_logger(__name__) +_background_tasks: set[asyncio.Task] = set() + router = APIRouter(prefix="/zai", tags=["zai"]) ZAI_API_BASE = "https://api.z.ai/api/anthropic" @@ -168,7 +170,7 @@ async def zai_messages_proxy( # This prevents race conditions where multiple requests persist the same message user_messages_to_persist = await check_for_duplicate_message(server, agent, actor, user_messages, PROXY_NAME) - asyncio.create_task( + task = asyncio.create_task( persist_messages_background( server=server, agent=agent, @@ -179,6 +181,8 @@ async def zai_messages_proxy( proxy_name=PROXY_NAME, ) ) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) return StreamingResponse( stream_response(), @@ -222,7 +226,7 @@ async def zai_messages_proxy( # Check for duplicate user messages before creating background task user_messages_to_persist = await check_for_duplicate_message(server, agent, actor, user_messages, PROXY_NAME) - asyncio.create_task( + task = asyncio.create_task( persist_messages_background( server=server, agent=agent, @@ -233,6 +237,8 @@ async def zai_messages_proxy( proxy_name=PROXY_NAME, ) ) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) except Exception as e: logger.warning(f"[{PROXY_NAME}] Failed to extract assistant response for logging: {e}") diff --git a/letta/server/rest_api/streaming_response.py b/letta/server/rest_api/streaming_response.py index db117786..9b7e5738 100644 --- a/letta/server/rest_api/streaming_response.py +++ b/letta/server/rest_api/streaming_response.py @@ -42,7 +42,7 @@ def get_cancellation_event_for_run(run_id: str) -> asyncio.Event: class RunCancelledException(Exception): """Exception raised when a run is explicitly cancelled (not due to client timeout)""" - def __init__(self, run_id: str, message: str = None): + def __init__(self, run_id: str, message: str | None = None): self.run_id = run_id super().__init__(message or f"Run {run_id} was explicitly cancelled") diff --git a/letta/server/rest_api/utils.py b/letta/server/rest_api/utils.py index dee13a01..bfbbe505 100644 --- a/letta/server/rest_api/utils.py +++ b/letta/server/rest_api/utils.py @@ -308,7 +308,7 @@ def create_approval_request_message_from_llm_response( reasoning_content: Optional[List[Union[TextContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent]]] = None, pre_computed_assistant_message_id: Optional[str] = None, step_id: str | None = None, - run_id: str = None, + run_id: str | None = None, ) -> Message: messages = [] if allowed_tool_calls: @@ -386,7 +386,7 @@ def create_letta_messages_from_llm_response( function_response: Optional[str], timezone: str, run_id: str | None = None, - step_id: str = None, + step_id: str | None = None, continue_stepping: bool = False, heartbeat_reason: Optional[str] = None, reasoning_content: Optional[ diff --git a/letta/server/server.py b/letta/server/server.py index 175c8819..8612d87d 100644 --- a/letta/server/server.py +++ b/letta/server/server.py @@ -891,7 +891,7 @@ class SyncServer(object): async def delete_archival_memory_async(self, memory_id: str, actor: User): # TODO check if it exists first, and throw error if not # TODO: need to also rebuild the prompt here - passage = await self.passage_manager.get_passage_by_id_async(passage_id=memory_id, actor=actor) + await self.passage_manager.get_passage_by_id_async(passage_id=memory_id, actor=actor) # delete the passage await self.passage_manager.delete_passage_by_id_async(passage_id=memory_id, actor=actor) @@ -1179,7 +1179,7 @@ class SyncServer(object): return None try: block = await self.agent_manager.get_block_with_label_async(agent_id=main_agent.id, block_label=source.name, actor=actor) - except: + except Exception: block = await self.block_manager.create_or_update_block_async(Block(label=source.name, value=""), actor=actor) await self.agent_manager.attach_block_async(agent_id=main_agent.id, block_id=block.id, actor=actor) diff --git a/letta/server/ws_api/server.py b/letta/server/ws_api/server.py index 67cadb0a..80e2b369 100644 --- a/letta/server/ws_api/server.py +++ b/letta/server/ws_api/server.py @@ -53,7 +53,7 @@ class WebSocketServer: # Assuming the message is a JSON string try: data = json_loads(message) - except: + except Exception: print(f"[server] bad data from client:\n{data}") await websocket.send(protocol.server_command_response(f"Error: bad data from client - {str(data)}")) continue diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index 5e4d2366..a3532a83 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -1514,7 +1514,7 @@ class AgentManager: @trace_method def trim_older_in_context_messages(self, num: int, agent_id: str, actor: PydanticUser) -> PydanticAgentState: message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids - new_messages = [message_ids[0]] + message_ids[num:] # 0 is system message + new_messages = [message_ids[0], *message_ids[num:]] return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor) @enforce_types diff --git a/letta/services/agent_serialization_manager.py b/letta/services/agent_serialization_manager.py index a9996b76..108ce9c2 100644 --- a/letta/services/agent_serialization_manager.py +++ b/letta/services/agent_serialization_manager.py @@ -161,7 +161,7 @@ class AgentSerializationManager: return sorted(unique_blocks.values(), key=lambda x: x.label) async def _extract_unique_sources_and_files_from_agents( - self, agent_states: List[AgentState], actor: User, files_agents_cache: dict = None + self, agent_states: List[AgentState], actor: User, files_agents_cache: dict | None = None ) -> tuple[List[Source], List[FileMetadata]]: """Extract unique sources and files from agent states using bulk operations""" @@ -192,7 +192,7 @@ class AgentSerializationManager: self, agent_state: AgentState, actor: User, - files_agents_cache: dict = None, + files_agents_cache: dict | None = None, scrub_messages: bool = False, ) -> AgentSchema: """Convert AgentState to AgentSchema with ID remapping""" diff --git a/letta/services/context_window_calculator/context_window_calculator.py b/letta/services/context_window_calculator/context_window_calculator.py index f4ad2e07..cfa3afe6 100644 --- a/letta/services/context_window_calculator/context_window_calculator.py +++ b/letta/services/context_window_calculator/context_window_calculator.py @@ -266,7 +266,7 @@ class ContextWindowCalculator: # Use provided message_ids or fall back to agent_state.message_ids[1:] effective_message_ids = message_ids if message_ids is not None else agent_state.message_ids[1:] messages = await message_manager.get_messages_by_ids_async(message_ids=effective_message_ids, actor=actor) - in_context_messages = [system_message_compiled] + messages + in_context_messages = [system_message_compiled, *messages] # Filter out None messages (can occur when system message is missing) original_count = len(in_context_messages) diff --git a/letta/services/file_manager.py b/letta/services/file_manager.py index 9b1fc0ab..eeab2aca 100644 --- a/letta/services/file_manager.py +++ b/letta/services/file_manager.py @@ -39,7 +39,9 @@ class DuplicateFileError(Exception): class FileManager: """Manager class to handle business logic related to files.""" - async def _invalidate_file_caches(self, file_id: str, actor: PydanticUser, original_filename: str = None, source_id: str = None): + async def _invalidate_file_caches( + self, file_id: str, actor: PydanticUser, original_filename: str | None = None, source_id: str | None = None + ): """Invalidate all caches related to a file.""" # TEMPORARILY DISABLED - caching is disabled # # invalidate file content cache (all variants) @@ -700,7 +702,7 @@ class FileManager: async with db_registry.async_session() as session: # We need to import FileAgent here to avoid circular imports - from letta.orm.file_agent import FileAgent as FileAgentModel + from letta.orm.files_agents import FileAgent as FileAgentModel # Join through file-agent relationships query = ( diff --git a/letta/services/file_processor/chunker/llama_index_chunker.py b/letta/services/file_processor/chunker/llama_index_chunker.py index ab6ea4a6..f653b062 100644 --- a/letta/services/file_processor/chunker/llama_index_chunker.py +++ b/letta/services/file_processor/chunker/llama_index_chunker.py @@ -146,7 +146,9 @@ class LlamaIndexChunker: raise e # Raise the original error @trace_method - def default_chunk_text(self, content: Union[OCRPageObject, str], chunk_size: int = None, chunk_overlap: int = None) -> List[str]: + def default_chunk_text( + self, content: Union[OCRPageObject, str], chunk_size: int | None = None, chunk_overlap: int | None = None + ) -> List[str]: """Chunk text using default SentenceSplitter regardless of file type with conservative defaults""" try: from llama_index.core.node_parser import SentenceSplitter diff --git a/letta/services/file_processor/embedder/openai_embedder.py b/letta/services/file_processor/embedder/openai_embedder.py index 4f979e1f..743559d8 100644 --- a/letta/services/file_processor/embedder/openai_embedder.py +++ b/letta/services/file_processor/embedder/openai_embedder.py @@ -136,7 +136,7 @@ class OpenAIEmbedder(BaseEmbedder): ) # Extract just the chunk text and indices for processing - chunk_indices = [i for i, _ in valid_chunks] + [i for i, _ in valid_chunks] chunks_to_embed = [chunk for _, chunk in valid_chunks] embedding_start = time.time() diff --git a/letta/services/files_agents_manager.py b/letta/services/files_agents_manager.py index 4d053ade..7cccc6d9 100644 --- a/letta/services/files_agents_manager.py +++ b/letta/services/files_agents_manager.py @@ -49,7 +49,7 @@ class FileAgentManager: """ if is_open: # Use the efficient LRU + open method - closed_files, was_already_open, _ = await self.enforce_max_open_files_and_open( + closed_files, _was_already_open, _ = await self.enforce_max_open_files_and_open( agent_id=agent_id, file_id=file_id, file_name=file_name, diff --git a/letta/services/group_manager.py b/letta/services/group_manager.py index 4e2fd58b..1a570846 100644 --- a/letta/services/group_manager.py +++ b/letta/services/group_manager.py @@ -238,7 +238,7 @@ class GroupManager: async def reset_messages_async(self, group_id: str, actor: PydanticUser) -> None: async with db_registry.async_session() as session: # Ensure group is loadable by user - group = await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor) + await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor) # Delete all messages in the group delete_stmt = delete(MessageModel).where( diff --git a/letta/services/helpers/agent_manager_helper.py b/letta/services/helpers/agent_manager_helper.py index c91e5bf6..eb313905 100644 --- a/letta/services/helpers/agent_manager_helper.py +++ b/letta/services/helpers/agent_manager_helper.py @@ -369,25 +369,17 @@ def initialize_message_sequence( # Some LMStudio models (e.g. meta-llama-3.1) require the user message before any tool calls if llm_config.provider_name == "lmstudio_openai": - messages = ( - [ - {"role": "system", "content": full_system_message}, - ] - + [ - {"role": "user", "content": first_user_message}, - ] - + initial_boot_messages - ) + messages = [ + {"role": "system", "content": full_system_message}, + {"role": "user", "content": first_user_message}, + *initial_boot_messages, + ] else: - messages = ( - [ - {"role": "system", "content": full_system_message}, - ] - + initial_boot_messages - + [ - {"role": "user", "content": first_user_message}, - ] - ) + messages = [ + {"role": "system", "content": full_system_message}, + *initial_boot_messages, + {"role": "user", "content": first_user_message}, + ] else: messages = [ @@ -442,25 +434,17 @@ async def initialize_message_sequence_async( # Some LMStudio models (e.g. meta-llama-3.1) require the user message before any tool calls if llm_config.provider_name == "lmstudio_openai": - messages = ( - [ - {"role": "system", "content": full_system_message}, - ] - + [ - {"role": "user", "content": first_user_message}, - ] - + initial_boot_messages - ) + messages = [ + {"role": "system", "content": full_system_message}, + {"role": "user", "content": first_user_message}, + *initial_boot_messages, + ] else: - messages = ( - [ - {"role": "system", "content": full_system_message}, - ] - + initial_boot_messages - + [ - {"role": "user", "content": first_user_message}, - ] - ) + messages = [ + {"role": "system", "content": full_system_message}, + *initial_boot_messages, + {"role": "user", "content": first_user_message}, + ] else: messages = [ diff --git a/letta/services/llm_trace_writer.py b/letta/services/llm_trace_writer.py index 169100a2..9e671d20 100644 --- a/letta/services/llm_trace_writer.py +++ b/letta/services/llm_trace_writer.py @@ -24,6 +24,8 @@ logger = get_logger(__name__) MAX_RETRIES = 3 INITIAL_BACKOFF_SECONDS = 1.0 +_background_tasks: set[asyncio.Task] = set() + def _parse_clickhouse_endpoint(endpoint: str) -> tuple[str, int, bool]: """Return (host, port, secure) for clickhouse_connect.get_client. @@ -129,11 +131,11 @@ class LLMTraceWriter: if not self._enabled or self._shutdown: return - # Fire-and-forget with create_task to not block the request path try: - asyncio.create_task(self._write_with_retry(trace)) + task = asyncio.create_task(self._write_with_retry(trace)) + _background_tasks.add(task) + task.add_done_callback(_background_tasks.discard) except RuntimeError: - # No running event loop (shouldn't happen in normal async context) pass async def _write_with_retry(self, trace: "LLMTrace") -> None: diff --git a/letta/services/mcp/oauth_utils.py b/letta/services/mcp/oauth_utils.py index 52599008..32114db5 100644 --- a/letta/services/mcp/oauth_utils.py +++ b/letta/services/mcp/oauth_utils.py @@ -4,7 +4,6 @@ import asyncio import json import secrets import time -import uuid from datetime import datetime, timedelta from typing import TYPE_CHECKING, Callable, Optional, Tuple @@ -94,16 +93,20 @@ class DatabaseTokenStorage(TokenStorage): class MCPOAuthSession: """Legacy OAuth session class - deprecated, use mcp_manager directly.""" - def __init__(self, server_url: str, server_name: str, user_id: Optional[str], organization_id: str): + def __init__( + self, + session_id: str, + server_url: Optional[str] = None, + server_name: Optional[str] = None, + user_id: Optional[str] = None, + organization_id: Optional[str] = None, + ): + self.session_id = session_id self.server_url = server_url self.server_name = server_name self.user_id = user_id self.organization_id = organization_id - self.session_id = str(uuid.uuid4()) - self.state = secrets.token_urlsafe(32) - - def __init__(self, session_id: str): - self.session_id = session_id + self.state = secrets.token_urlsafe(32) if server_url else None # TODO: consolidate / deprecate this in favor of mcp_manager access async def create_session(self) -> str: diff --git a/letta/services/mcp_manager.py b/letta/services/mcp_manager.py index 32b91ca8..b22db1eb 100644 --- a/letta/services/mcp_manager.py +++ b/letta/services/mcp_manager.py @@ -403,7 +403,7 @@ class MCPManager: # context manager now handles commits # await session.commit() return mcp_server.to_pydantic() - except Exception as e: + except Exception: await session.rollback() raise @@ -1193,7 +1193,7 @@ class MCPManager: # Give the OAuth flow time to connect to the MCP server and store the authorization URL timeout = 0 - while not auth_session or not auth_session.authorization_url and not connect_task.done() and timeout < 10: + while not auth_session or (not auth_session.authorization_url and not connect_task.done() and timeout < 10): timeout += 1 auth_session = await self.get_oauth_session_by_id(session_id, actor) await asyncio.sleep(1.0) diff --git a/letta/services/mcp_server_manager.py b/letta/services/mcp_server_manager.py index 462da216..70cbe651 100644 --- a/letta/services/mcp_server_manager.py +++ b/letta/services/mcp_server_manager.py @@ -500,7 +500,7 @@ class MCPServerManager: # context manager now handles commits # await session.commit() return mcp_server.to_pydantic() - except Exception as e: + except Exception: await session.rollback() raise diff --git a/letta/services/memory_repo/__init__.py b/letta/services/memory_repo/__init__.py index fb7d5e97..95148a52 100644 --- a/letta/services/memory_repo/__init__.py +++ b/letta/services/memory_repo/__init__.py @@ -12,9 +12,9 @@ except ImportError: from letta.services.memory_repo.memfs_client_base import MemfsClient __all__ = [ - "MemoryRepoManager", - "MemfsClient", - "StorageBackend", "GCSStorageBackend", "LocalStorageBackend", + "MemfsClient", + "MemoryRepoManager", + "StorageBackend", ] diff --git a/letta/services/memory_repo/git_operations.py b/letta/services/memory_repo/git_operations.py index a3102200..ba4080b5 100644 --- a/letta/services/memory_repo/git_operations.py +++ b/letta/services/memory_repo/git_operations.py @@ -96,7 +96,7 @@ class GitOperations: os.makedirs(repo_path) # Initialize a new repository - repo = dulwich.repo.Repo.init(repo_path) + dulwich.repo.Repo.init(repo_path) # Use `main` as the default branch (git's modern default). head_path = os.path.join(repo_path, ".git", "HEAD") diff --git a/letta/services/memory_repo/memfs_client_base.py b/letta/services/memory_repo/memfs_client_base.py index 6a7a9e32..c58d36f2 100644 --- a/letta/services/memory_repo/memfs_client_base.py +++ b/letta/services/memory_repo/memfs_client_base.py @@ -40,7 +40,7 @@ class MemfsClient: This enables git-backed memory for self-hosted OSS deployments. """ - def __init__(self, base_url: str = None, local_path: str = None, timeout: float = 120.0): + def __init__(self, base_url: str | None = None, local_path: str | None = None, timeout: float = 120.0): """Initialize the local memfs client. Args: @@ -68,7 +68,7 @@ class MemfsClient: self, agent_id: str, actor: PydanticUser, - initial_blocks: List[PydanticBlock] = None, + initial_blocks: List[PydanticBlock] | None = None, ) -> str: """Create a new repository for an agent with optional initial blocks. diff --git a/letta/services/memory_repo/storage/__init__.py b/letta/services/memory_repo/storage/__init__.py index 08db7a97..756125b9 100644 --- a/letta/services/memory_repo/storage/__init__.py +++ b/letta/services/memory_repo/storage/__init__.py @@ -5,7 +5,7 @@ from letta.services.memory_repo.storage.gcs import GCSStorageBackend from letta.services.memory_repo.storage.local import LocalStorageBackend __all__ = [ - "StorageBackend", "GCSStorageBackend", "LocalStorageBackend", + "StorageBackend", ] diff --git a/letta/services/passage_manager.py b/letta/services/passage_manager.py index 55ba29f3..171dffea 100644 --- a/letta/services/passage_manager.py +++ b/letta/services/passage_manager.py @@ -1,6 +1,9 @@ import uuid from datetime import datetime, timezone -from typing import Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional + +if TYPE_CHECKING: + from letta.orm.sqlalchemy_base import SqlalchemyBase from openai import AsyncOpenAI from sqlalchemy import func, select @@ -350,7 +353,7 @@ class PassageManager: return passage.to_pydantic() @trace_method - def _preprocess_passage_for_creation(self, pydantic_passage: PydanticPassage) -> "SqlAlchemyBase": # noqa: F821 + def _preprocess_passage_for_creation(self, pydantic_passage: PydanticPassage) -> "SqlalchemyBase": data = pydantic_passage.model_dump(to_orm=True) common_fields = { "id": data.get("id"), @@ -364,13 +367,13 @@ class PassageManager: "created_at": data.get("created_at", datetime.now(timezone.utc)), } - if "archive_id" in data and data["archive_id"]: + if data.get("archive_id"): assert not data.get("source_id"), "Passage cannot have both archive_id and source_id" agent_fields = { "archive_id": data["archive_id"], } passage = ArchivalPassage(**common_fields, **agent_fields) - elif "source_id" in data and data["source_id"]: + elif data.get("source_id"): assert not data.get("archive_id"), "Passage cannot have both archive_id and source_id" source_fields = { "source_id": data["source_id"], @@ -693,7 +696,7 @@ class PassageManager: setattr(curr_passage, "tags", new_tags) # Pad embeddings if needed (only when using Postgres as vector DB) - if "embedding" in update_data and update_data["embedding"]: + if update_data.get("embedding"): import numpy as np from letta.helpers.tpuf_client import should_use_tpuf @@ -738,7 +741,7 @@ class PassageManager: update_data = passage.model_dump(to_orm=True, exclude_unset=True, exclude_none=True) # Pad embeddings if needed (only when using Postgres as vector DB) - if "embedding" in update_data and update_data["embedding"]: + if update_data.get("embedding"): import numpy as np from letta.helpers.tpuf_client import should_use_tpuf diff --git a/letta/services/provider_manager.py b/letta/services/provider_manager.py index 453fcf1a..d1c98dc5 100644 --- a/letta/services/provider_manager.py +++ b/letta/services/provider_manager.py @@ -409,7 +409,7 @@ class ProviderManager: try: provider_model = await ProviderModel.read_async(db_session=session, identifier=provider_id, actor=actor) return provider_model.to_pydantic() - except: + except Exception: # If not found, try to get as global provider (organization_id=NULL) from sqlalchemy import select @@ -1048,7 +1048,7 @@ class ProviderManager: # Model not in DB - check if it's from a BYOK provider # Handle format is "provider_name/model_name" if "/" in handle: - provider_name, model_name = handle.split("/", 1) + provider_name, _model_name = handle.split("/", 1) byok_providers = await self.list_providers_async( actor=actor, name=provider_name, diff --git a/letta/services/run_manager.py b/letta/services/run_manager.py index 26650b71..7283b701 100644 --- a/letta/services/run_manager.py +++ b/letta/services/run_manager.py @@ -739,7 +739,7 @@ class RunManager: ) # Combine approval response and tool messages - new_messages = approval_response_messages + [tool_message] + new_messages = [*approval_response_messages, tool_message] # Checkpoint the new messages from letta.agents.agent_loop import AgentLoop diff --git a/letta/services/streaming_service.py b/letta/services/streaming_service.py index 85fca482..beac6eda 100644 --- a/letta/services/streaming_service.py +++ b/letta/services/streaming_service.py @@ -322,7 +322,6 @@ class StreamingService: async def error_aware_stream(): """Stream that handles early LLM errors gracefully in streaming format.""" run_status = None - run_update_metadata = None stop_reason = None error_data = None saw_done = False @@ -441,7 +440,7 @@ class StreamingService: yield f"event: error\ndata: {error_message.model_dump_json()}\n\n" # Send [DONE] marker to properly close the stream yield "data: [DONE]\n\n" - except RunCancelledException as e: + except RunCancelledException: # Run was explicitly cancelled - this is not an error # The cancellation has already been handled by cancellation_aware_stream_wrapper logger.info(f"Run {run_id} was cancelled, exiting stream gracefully") diff --git a/letta/services/summarizer/summarizer.py b/letta/services/summarizer/summarizer.py index 4206e6cd..28ff39d0 100644 --- a/letta/services/summarizer/summarizer.py +++ b/letta/services/summarizer/summarizer.py @@ -1,5 +1,9 @@ import json -from typing import List, Optional, Tuple, Union +from typing import TYPE_CHECKING, List, Optional, Tuple, Union + +if TYPE_CHECKING: + from letta.agents.voice_sleeptime_agent import VoiceSleeptimeAgent + from letta.services.telemetry_manager import TelemetryManager from letta.agents.ephemeral_summary_agent import EphemeralSummaryAgent from letta.constants import ( @@ -39,7 +43,7 @@ class Summarizer: def __init__( self, mode: SummarizationMode, - summarizer_agent: Optional[Union[EphemeralSummaryAgent, "VoiceSleeptimeAgent"]] = None, # noqa: F821 + summarizer_agent: Optional[Union[EphemeralSummaryAgent, "VoiceSleeptimeAgent"]] = None, message_buffer_limit: int = 10, message_buffer_min: int = 3, partial_evict_summarizer_percentage: float = 0.30, @@ -235,7 +239,7 @@ class Summarizer: ) updated_in_context_messages = all_in_context_messages[assistant_message_index:] - return [all_in_context_messages[0], summary_message_obj] + updated_in_context_messages, True + return [all_in_context_messages[0], summary_message_obj, *updated_in_context_messages], True def _static_buffer_summarization( self, @@ -336,7 +340,7 @@ class Summarizer: self.summarizer_agent.step([MessageCreate(role=MessageRole.user, content=[TextContent(text=summary_request_text)])]) ) - return [all_in_context_messages[0]] + updated_in_context_messages, True + return [all_in_context_messages[0], *updated_in_context_messages], True def simple_formatter( @@ -451,7 +455,7 @@ async def simple_summary( actor: User, include_ack: bool = True, prompt: str | None = None, - telemetry_manager: "TelemetryManager | None" = None, # noqa: F821 + telemetry_manager: "TelemetryManager | None" = None, agent_id: str | None = None, agent_tags: List[str] | None = None, run_id: str | None = None, diff --git a/letta/services/summarizer/summarizer_all.py b/letta/services/summarizer/summarizer_all.py index fc183214..aa3818d0 100644 --- a/letta/services/summarizer/summarizer_all.py +++ b/letta/services/summarizer/summarizer_all.py @@ -79,4 +79,4 @@ async def summarize_all( logger.warning(f"Summary length {len(summary_message_str)} exceeds clip length {summarizer_config.clip_chars}. Truncating.") summary_message_str = summary_message_str[: summarizer_config.clip_chars] + "... [summary truncated to fit]" - return summary_message_str, [in_context_messages[0]] + protected_messages + return summary_message_str, [in_context_messages[0], *protected_messages] diff --git a/letta/services/summarizer/summarizer_sliding_window.py b/letta/services/summarizer/summarizer_sliding_window.py index d1ff5186..0425c2d4 100644 --- a/letta/services/summarizer/summarizer_sliding_window.py +++ b/letta/services/summarizer/summarizer_sliding_window.py @@ -1,4 +1,7 @@ -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING, List, Optional, Tuple + +if TYPE_CHECKING: + from letta.schemas.tool import Tool from letta.log import get_logger from letta.otel.tracing import trace_method @@ -41,7 +44,7 @@ async def count_tokens_with_tools( actor: User, llm_config: LLMConfig, messages: List[Message], - tools: Optional[List["Tool"]] = None, # noqa: F821 + tools: Optional[List["Tool"]] = None, ) -> int: """Count tokens in messages AND tool definitions. @@ -125,7 +128,7 @@ async def summarize_via_sliding_window( maximum_message_index = total_message_count - 1 # Starts at N% (eg 70%), and increments up until 100% - message_count_cutoff_percent = max( + max( 1 - summarizer_config.sliding_window_percentage, 0.10 ) # Some arbitrary minimum value (10%) to avoid negatives from badly configured summarizer percentage eviction_percentage = summarizer_config.sliding_window_percentage @@ -170,7 +173,7 @@ async def summarize_via_sliding_window( # update token count logger.info(f"Attempting to compact messages index 1:{assistant_message_index} messages") - post_summarization_buffer = [system_prompt] + in_context_messages[assistant_message_index:] + post_summarization_buffer = [system_prompt, *in_context_messages[assistant_message_index:]] approx_token_count = await count_tokens(actor, agent_llm_config, post_summarization_buffer) logger.info( f"Compacting messages index 1:{assistant_message_index} messages resulted in {approx_token_count} tokens, goal is {goal_tokens}" @@ -214,4 +217,4 @@ async def summarize_via_sliding_window( summary_message_str = summary_message_str[: summarizer_config.clip_chars] + "... [summary truncated to fit]" updated_in_context_messages = in_context_messages[assistant_message_index:] - return summary_message_str, [system_prompt] + updated_in_context_messages + return summary_message_str, [system_prompt, *updated_in_context_messages] diff --git a/letta/services/tool_executor/core_tool_executor.py b/letta/services/tool_executor/core_tool_executor.py index f46c0d9b..e1896731 100644 --- a/letta/services/tool_executor/core_tool_executor.py +++ b/letta/services/tool_executor/core_tool_executor.py @@ -619,7 +619,7 @@ class LettaCoreToolExecutor(ToolExecutor): if kind == "add": try: - existing = agent_state.memory.get_block(action["label"]) + agent_state.memory.get_block(action["label"]) # If we get here, the block exists raise ValueError(f"Error: Memory block '{action['label']}' already exists") except KeyError: @@ -724,7 +724,7 @@ class LettaCoreToolExecutor(ToolExecutor): # Collate into the new value to update new_value = "\n".join(new_value_lines) - snippet = "\n".join(snippet_lines) + "\n".join(snippet_lines) # Write into the block agent_state.memory.update_block_value(label=label, value=new_value) @@ -985,7 +985,7 @@ class LettaCoreToolExecutor(ToolExecutor): # Collate into the new value to update new_value = "\n".join(new_value_lines) - snippet = "\n".join(snippet_lines) + "\n".join(snippet_lines) # Write into the block await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(value=new_value), actor=actor) diff --git a/letta/services/tool_executor/files_tool_executor.py b/letta/services/tool_executor/files_tool_executor.py index d05d42f3..ed0b40b9 100644 --- a/letta/services/tool_executor/files_tool_executor.py +++ b/letta/services/tool_executor/files_tool_executor.py @@ -189,7 +189,7 @@ class LettaFileToolExecutor(ToolExecutor): visible_content = "\n".join(content_lines) # Handle LRU eviction and file opening - closed_files, was_already_open, previous_ranges = await self.files_agents_manager.enforce_max_open_files_and_open( + closed_files, _was_already_open, previous_ranges = await self.files_agents_manager.enforce_max_open_files_and_open( agent_id=agent_state.id, file_id=file_id, file_name=file_name, @@ -683,7 +683,7 @@ class LettaFileToolExecutor(ToolExecutor): summary = f"Found {total_hits} matches in {file_count} file{'s' if file_count != 1 else ''} for query: '{query}'" # combine all results - formatted_results = [summary, "=" * len(summary)] + results + formatted_results = [summary, "=" * len(summary), *results] self.logger.info(f"Turbopuffer search completed: {total_hits} matches across {file_count} files") return "\n".join(formatted_results) @@ -780,7 +780,7 @@ class LettaFileToolExecutor(ToolExecutor): summary = f"Found {total_hits} Pinecone matches in {file_count} file{'s' if file_count != 1 else ''} for query: '{query}'" # Combine all results - formatted_results = [summary, "=" * len(summary)] + results + formatted_results = [summary, "=" * len(summary), *results] self.logger.info(f"Pinecone search completed: {total_hits} matches across {file_count} files") return "\n".join(formatted_results) @@ -846,7 +846,7 @@ class LettaFileToolExecutor(ToolExecutor): summary = f"Found {total_passages} semantic matches in {file_count} file{'s' if file_count != 1 else ''} for query: '{query}'" # Combine all results - formatted_results = [summary, "=" * len(summary)] + results + formatted_results = [summary, "=" * len(summary), *results] self.logger.info(f"Semantic search completed: {total_passages} matches across {file_count} files") diff --git a/letta/services/tool_executor/tool_execution_manager.py b/letta/services/tool_executor/tool_execution_manager.py index bffce487..60d7f2a4 100644 --- a/letta/services/tool_executor/tool_execution_manager.py +++ b/letta/services/tool_executor/tool_execution_manager.py @@ -1,7 +1,7 @@ import asyncio import json import traceback -from typing import Any, Dict, Optional, Type +from typing import Any, ClassVar, Dict, Optional, Type from letta.constants import FUNCTION_RETURN_VALUE_TRUNCATED from letta.helpers.datetime_helpers import AsyncTimer @@ -33,7 +33,7 @@ from letta.utils import get_friendly_error_msg class ToolExecutorFactory: """Factory for creating appropriate tool executors based on tool type.""" - _executor_map: Dict[ToolType, Type[ToolExecutor]] = { + _executor_map: ClassVar[Dict[ToolType, Type[ToolExecutor]]] = { ToolType.LETTA_CORE: LettaCoreToolExecutor, ToolType.LETTA_MEMORY_CORE: LettaCoreToolExecutor, ToolType.LETTA_SLEEPTIME_CORE: LettaCoreToolExecutor, diff --git a/letta/services/tool_executor/tool_execution_sandbox.py b/letta/services/tool_executor/tool_execution_sandbox.py index c619274e..ebc18dd6 100644 --- a/letta/services/tool_executor/tool_execution_sandbox.py +++ b/letta/services/tool_executor/tool_execution_sandbox.py @@ -7,7 +7,10 @@ import sys import tempfile import traceback import uuid -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from e2b_code_interpreter import Execution, Sandbox from letta.functions.helpers import generate_model_from_args_json_schema from letta.log import get_logger @@ -256,7 +259,7 @@ class ToolExecutionSandbox: temp_file_path: str, ) -> ToolExecutionResult: status = "success" - func_return, agent_state, stderr = None, None, None + func_return, agent_state, _stderr = None, None, None old_stdout = sys.stdout old_stderr = sys.stderr @@ -409,13 +412,13 @@ class ToolExecutionSandbox: sandbox_config_fingerprint=sbx_config.fingerprint(), ) - def parse_exception_from_e2b_execution(self, e2b_execution: "Execution") -> Exception: # noqa: F821 + def parse_exception_from_e2b_execution(self, e2b_execution: "Execution") -> Exception: builtins_dict = __builtins__ if isinstance(__builtins__, dict) else vars(__builtins__) # Dynamically fetch the exception class from builtins, defaulting to Exception if not found exception_class = builtins_dict.get(e2b_execution.error.name, Exception) return exception_class(e2b_execution.error.value) - def get_running_e2b_sandbox_with_same_state(self, sandbox_config: SandboxConfig) -> Optional["Sandbox"]: # noqa: F821 + def get_running_e2b_sandbox_with_same_state(self, sandbox_config: SandboxConfig) -> Optional["Sandbox"]: from e2b_code_interpreter import Sandbox # List running sandboxes and access metadata. @@ -430,7 +433,7 @@ class ToolExecutionSandbox: return None @trace_method - def create_e2b_sandbox_with_metadata_hash(self, sandbox_config: SandboxConfig) -> "Sandbox": # noqa: F821 + def create_e2b_sandbox_with_metadata_hash(self, sandbox_config: SandboxConfig) -> "Sandbox": from e2b_code_interpreter import Sandbox state_hash = sandbox_config.fingerprint() diff --git a/letta/services/tool_manager.py b/letta/services/tool_manager.py index 2613c99b..3833d506 100644 --- a/letta/services/tool_manager.py +++ b/letta/services/tool_manager.py @@ -47,7 +47,7 @@ logger = get_logger(__name__) # NOTE: function name and nested modal function decorator name must stay in sync with MODAL_DEFAULT_TOOL_NAME -def modal_tool_wrapper(tool: PydanticTool, actor: PydanticUser, sandbox_env_vars: dict = None, project_id: str = "default"): +def modal_tool_wrapper(tool: PydanticTool, actor: PydanticUser, sandbox_env_vars: dict | None = None, project_id: str = "default"): """Create a Modal function wrapper for a tool""" import contextlib import io @@ -183,7 +183,7 @@ def modal_tool_wrapper(tool: PydanticTool, actor: PydanticUser, sandbox_env_vars result = asyncio.run(tool_func(**kwargs)) else: result = tool_func(**kwargs) - except Exception as e: + except Exception: # Capture the exception and write to stderr error_occurred = True traceback.print_exc(file=stderr_capture) @@ -1342,7 +1342,7 @@ class ToolManager: """Delete a Modal app deployment for the tool""" try: # Generate the app name for this tool - modal_app_name = generate_modal_function_name(tool.id, actor.organization_id) + generate_modal_function_name(tool.id, actor.organization_id) # Try to delete the app # TODO: we need to soft delete, and then potentially stop via the CLI, no programmatic way to delete currently diff --git a/letta/services/tool_sandbox/base.py b/letta/services/tool_sandbox/base.py index 9b290f8c..42ff012d 100644 --- a/letta/services/tool_sandbox/base.py +++ b/letta/services/tool_sandbox/base.py @@ -202,7 +202,7 @@ class AsyncToolSandboxBase(ABC): lines.append("import asyncio") if inject_agent_state: - lines.extend(["import letta", "from letta import *"]) # noqa: F401 + lines.extend(["import letta", "from letta import *"]) # Import Letta client if available (wrapped in try/except for sandboxes without letta_client installed) if inject_letta_client: @@ -438,7 +438,7 @@ class AsyncToolSandboxBase(ABC): if isinstance(node, ast.AsyncFunctionDef) and node.name == self.tool.name: return True return False - except: + except Exception: return False def use_top_level_await(self) -> bool: diff --git a/letta/services/tool_sandbox/modal_sandbox_v2.py b/letta/services/tool_sandbox/modal_sandbox_v2.py index 488df05f..fc608bdd 100644 --- a/letta/services/tool_sandbox/modal_sandbox_v2.py +++ b/letta/services/tool_sandbox/modal_sandbox_v2.py @@ -192,7 +192,7 @@ class AsyncToolSandboxModalV2(AsyncToolSandboxBase): log_event("modal_v2_deploy_already_exists", {"app_name": app_full_name, "version": version}) # Return the created app with the function attached return app - except: + except Exception: # App doesn't exist, need to deploy pass diff --git a/letta/services/tool_sandbox/safe_pickle.py b/letta/services/tool_sandbox/safe_pickle.py index b27ef985..86c36f69 100644 --- a/letta/services/tool_sandbox/safe_pickle.py +++ b/letta/services/tool_sandbox/safe_pickle.py @@ -184,7 +184,7 @@ def sanitize_for_pickle(obj: Any) -> Any: # Test if the value is pickleable pickle.dumps(value, protocol=PICKLE_PROTOCOL) sanitized[key] = value - except: + except Exception: sanitized[key] = str(value) return sanitized diff --git a/letta/streaming_interface.py b/letta/streaming_interface.py index 83d5e2c6..ec447cc7 100644 --- a/letta/streaming_interface.py +++ b/letta/streaming_interface.py @@ -334,7 +334,7 @@ class StreamingRefreshCLIInterface(AgentRefreshStreamingInterface): if self.separate_send_message and function_name == "send_message": try: message = json.loads(function_args)["message"] - except: + except Exception: prefix = '{\n "message": "' if len(function_args) < len(prefix): message = "..." diff --git a/letta/system.py b/letta/system.py index e766420b..5b1095f5 100644 --- a/letta/system.py +++ b/letta/system.py @@ -175,7 +175,7 @@ def package_system_message(system_message, timezone, message_type="system_alert" if "type" in message_json and message_json["type"] == message_type: logger.warning(f"Attempted to pack a system message that is already packed. Not packing: '{system_message}'") return system_message - except: + except Exception: pass # do nothing, expected behavior that the message is not JSON formatted_time = get_local_time(timezone=timezone) @@ -260,7 +260,7 @@ def unpack_message(packed_message: str) -> str: message_json = json.loads(packed_message) if type(message_json) is not dict: return packed_message - except: + except Exception: return packed_message if "message" not in message_json: @@ -272,7 +272,7 @@ def unpack_message(packed_message: str) -> str: else: try: message_type = message_json["type"] - except: + except Exception: return packed_message if message_type != "user_message": diff --git a/letta/utils.py b/letta/utils.py index 85587e1a..852b4670 100644 --- a/letta/utils.py +++ b/letta/utils.py @@ -17,7 +17,7 @@ from contextlib import contextmanager from datetime import datetime, timezone from functools import wraps from logging import Logger -from typing import Any, Callable, Coroutine, Optional, Union, _GenericAlias, get_args, get_origin, get_type_hints +from typing import Any, Callable, Optional, Union, _GenericAlias, get_args, get_origin, get_type_hints # type: ignore[attr-defined] from urllib.parse import urljoin, urlparse import demjson3 as demjson diff --git a/pyproject.toml b/pyproject.toml index da83dceb..286ac711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,8 @@ dependencies = [ "certifi>=2025.6.15", "markitdown[docx,pdf,pptx]>=0.1.2", "orjson>=3.11.1", - "ruff[dev]>=0.12.10", + "ruff>=0.12.10", + "ty>=0.0.17", "trafilatura", "readability-lxml", "google-genai>=1.52.0", @@ -77,6 +78,7 @@ dependencies = [ "clickhouse-connect>=0.10.0", "aiofiles>=24.1.0", "async-lru>=2.0.5", + ] [project.scripts] @@ -182,20 +184,41 @@ extend-exclude = [ [tool.ruff.lint] select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "RUF", # ruff + "FAST", # fastapi ] ignore = [ - "E501", # line too long (handled by formatter) - "E402", # module import not at top of file - "E711", # none-comparison - "E712", # true-false-comparison - "E722", # bare except - "E721", # type comparison - "F811", # redefined while unused - "F841", # local variable assigned but never used + "E402", # module level import not at top of file + "E501", # line too long (handled by formatter) + "E711", # comparison to None (SQLAlchemy requires ==) + "E712", # comparison to True/False (SQLAlchemy requires ==) + "FAST002", # FastAPI dependency without Annotated (large migration) + "RUF001", # ambiguous unicode character in string + "RUF002", # ambiguous unicode character in docstring + "RUF003", # ambiguous unicode character in comment + "RUF010", # explicit conversion flag in f-string +] + +[tool.ty.rules] +all = "ignore" +unresolved-reference = "error" +unresolved-import = "ignore" + +[tool.ty.src] +exclude = ["examples/", "tests/data/"] + +[tool.ty.analysis] +allowed-unresolved-imports = [ + "letta_client.**", + "cowsay", + "transformers", + "core.utils", + "core.menu", + "some_nonexistent_package", ] [tool.ruff.lint.isort] diff --git a/sandbox/modal_executor.py b/sandbox/modal_executor.py index 2b759967..3c453fc0 100644 --- a/sandbox/modal_executor.py +++ b/sandbox/modal_executor.py @@ -225,7 +225,7 @@ def setup_signal_handlers(): # Enable fault handler with file output try: faulthandler.enable(file=sys.stderr, all_threads=True) - except: + except Exception: pass # Faulthandler might not be available # Set resource limits to prevent runaway processes @@ -234,7 +234,7 @@ def setup_signal_handlers(): resource.setrlimit(resource.RLIMIT_AS, (1024 * 1024 * 1024, 1024 * 1024 * 1024)) # Limit stack size to 8MB (default is often unlimited) resource.setrlimit(resource.RLIMIT_STACK, (8 * 1024 * 1024, 8 * 1024 * 1024)) - except: + except Exception: pass # Resource limits might not be available # Set environment variables diff --git a/tests/helpers/client_helper.py b/tests/helpers/client_helper.py index 99740d54..8a45b208 100644 --- a/tests/helpers/client_helper.py +++ b/tests/helpers/client_helper.py @@ -1,6 +1,6 @@ import time -from letta import RESTClient +from letta import RESTClient # type: ignore[attr-defined] from letta.schemas.enums import JobStatus from letta.schemas.job import Job from letta.schemas.source import Source diff --git a/tests/helpers/endpoints_helper.py b/tests/helpers/endpoints_helper.py index b169427c..a9a5ab70 100644 --- a/tests/helpers/endpoints_helper.py +++ b/tests/helpers/endpoints_helper.py @@ -142,7 +142,7 @@ def assert_invoked_send_message_with_keyword(messages: Sequence[LettaMessage], k send_message_function_call = target_message.tool_call try: arguments = json.loads(send_message_function_call.arguments) - except: + except Exception: raise InvalidToolCallError(messages=[target_message], explanation="Function call arguments could not be loaded into JSON") # Message field not in send_message diff --git a/tests/integration_test_builtin_tools.py b/tests/integration_test_builtin_tools.py index 9460b8a6..fad8b917 100644 --- a/tests/integration_test_builtin_tools.py +++ b/tests/integration_test_builtin_tools.py @@ -234,9 +234,9 @@ async def test_web_search() -> None: # Check for education-related information in summary and highlights result_text = "" - if "summary" in result and result["summary"]: + if result.get("summary"): result_text += " " + result["summary"].lower() - if "highlights" in result and result["highlights"]: + if result.get("highlights"): for highlight in result["highlights"]: result_text += " " + highlight.lower() diff --git a/tests/integration_test_client_side_tools.py b/tests/integration_test_client_side_tools.py index f9339bc7..b7027c4a 100644 --- a/tests/integration_test_client_side_tools.py +++ b/tests/integration_test_client_side_tools.py @@ -318,7 +318,7 @@ def get_secret_code(input_text: str) -> str: print(" ✓ Without client_tools, server tool executed directly (no approval required)") # The response should eventually contain the server value - all_content = " ".join([msg.content for msg in response4.messages if hasattr(msg, "content") and msg.content]) + " ".join([msg.content for msg in response4.messages if hasattr(msg, "content") and msg.content]) tool_returns = [msg for msg in response4.messages if msg.message_type == "tool_return_message"] if tool_returns: server_return_value = tool_returns[0].tool_return diff --git a/tests/integration_test_human_in_the_loop.py b/tests/integration_test_human_in_the_loop.py index 269060a3..aa3484a4 100644 --- a/tests/integration_test_human_in_the_loop.py +++ b/tests/integration_test_human_in_the_loop.py @@ -362,7 +362,7 @@ def test_invoke_tool_after_turning_off_requires_approval( try: assert messages[idx].message_type == "assistant_message" idx += 1 - except: + except Exception: pass assert messages[idx].message_type == "tool_call_message" @@ -375,7 +375,7 @@ def test_invoke_tool_after_turning_off_requires_approval( try: assert messages[idx].message_type == "assistant_message" idx += 1 - except: + except Exception: assert messages[idx].message_type == "tool_call_message" idx += 1 assert messages[idx].message_type == "tool_return_message" @@ -1324,7 +1324,7 @@ def test_agent_records_last_stop_reason_after_approval_flow( assert agent_after_approval.last_stop_reason != initial_stop_reason # Should be different from initial # Send follow-up message to complete the flow - response2 = client.agents.messages.create( + client.agents.messages.create( agent_id=agent.id, messages=USER_MESSAGE_FOLLOW_UP, ) diff --git a/tests/integration_test_multi_agent.py b/tests/integration_test_multi_agent.py index 92fa120c..37666239 100644 --- a/tests/integration_test_multi_agent.py +++ b/tests/integration_test_multi_agent.py @@ -86,7 +86,7 @@ def remove_stale_agents(client): @pytest.fixture(scope="function") def agent_obj(client: Letta) -> AgentState: """Create a test agent that we can call functions on""" - send_message_to_agent_tool = list(client.tools.list(name="send_message_to_agent_and_wait_for_reply"))[0] + send_message_to_agent_tool = next(iter(client.tools.list(name="send_message_to_agent_and_wait_for_reply"))) agent_state_instance = client.agents.create( agent_type="letta_v1_agent", include_base_tools=True, @@ -218,7 +218,7 @@ def test_send_message_to_agents_with_tags_simple(client: Letta): secret_word = "banana" # Create "manager" agent - send_message_to_agents_matching_tags_tool_id = list(client.tools.list(name="send_message_to_agents_matching_tags"))[0].id + send_message_to_agents_matching_tags_tool_id = next(iter(client.tools.list(name="send_message_to_agents_matching_tags"))).id manager_agent_state = client.agents.create( agent_type="letta_v1_agent", name="manager_agent", @@ -329,7 +329,7 @@ def test_send_message_to_agents_with_tags_complex_tool_use(client: Letta, roll_d test_id = str(uuid.uuid4())[:8] # Create "manager" agent - send_message_to_agents_matching_tags_tool_id = list(client.tools.list(name="send_message_to_agents_matching_tags"))[0].id + send_message_to_agents_matching_tags_tool_id = next(iter(client.tools.list(name="send_message_to_agents_matching_tags"))).id manager_agent_state = client.agents.create( agent_type="letta_v1_agent", tool_ids=[send_message_to_agents_matching_tags_tool_id], diff --git a/tests/integration_test_send_message.py b/tests/integration_test_send_message.py index 404965f2..488a9d8c 100644 --- a/tests/integration_test_send_message.py +++ b/tests/integration_test_send_message.py @@ -370,7 +370,7 @@ def assert_greeting_with_assistant_message_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -508,7 +508,7 @@ def assert_greeting_without_assistant_message_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -664,7 +664,7 @@ def assert_tool_call_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -700,7 +700,7 @@ def assert_tool_call_response( assert isinstance(messages[index], (ReasoningMessage, HiddenReasoningMessage)) assert messages[index].otid and messages[index].otid[-1] == "0" index += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -856,7 +856,7 @@ def assert_image_input_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -1889,7 +1889,7 @@ def test_async_greeting_with_assistant_message( messages_page = client.runs.messages.list(run_id=run.id) messages = messages_page.items - usage = client.runs.usage.retrieve(run_id=run.id) + client.runs.usage.retrieve(run_id=run.id) # TODO: add results API test later assert_greeting_with_assistant_message_response(messages, model_handle, model_settings, from_db=True) # TODO: remove from_db=True later @@ -2267,7 +2267,7 @@ def test_job_creation_for_send_message( assert len(new_runs) == 1 for run in runs: - if run.id == list(new_runs)[0]: + if run.id == next(iter(new_runs)): assert run.status == "completed" diff --git a/tests/integration_test_send_message_v2.py b/tests/integration_test_send_message_v2.py index 4ef0cd02..b91db8cf 100644 --- a/tests/integration_test_send_message_v2.py +++ b/tests/integration_test_send_message_v2.py @@ -25,6 +25,8 @@ from letta_client.types.agents.letta_streaming_response import LettaPing, LettaS logger = logging.getLogger(__name__) +_background_tasks: set[asyncio.Task] = set() + # ------------------------------ # Helper Functions and Constants @@ -132,7 +134,7 @@ def assert_greeting_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -203,7 +205,7 @@ def assert_tool_call_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -256,7 +258,7 @@ def assert_tool_call_response( assert messages[index].otid and messages[index].otid[-1] == str(otid_suffix) index += 1 otid_suffix += 1 - except: + except Exception: # Reasoning is non-deterministic, so don't throw if missing pass @@ -890,8 +892,10 @@ async def test_tool_call( agent_state = await client.agents.update(agent_id=agent_state.id, model=model_handle, model_settings=model_settings) if cancellation == "with_cancellation": - delay = 5 if "gpt-5" in model_handle else 0.5 # increase delay for responses api + delay = 5 if "gpt-5" in model_handle else 0.5 _cancellation_task = asyncio.create_task(cancel_run_after_delay(client, agent_state.id, delay=delay)) + _background_tasks.add(_cancellation_task) + _cancellation_task.add_done_callback(_background_tasks.discard) if send_type == "step": response = await client.agents.messages.create( diff --git a/tests/integration_test_summarizer.py b/tests/integration_test_summarizer.py index 5e437197..83726e3a 100644 --- a/tests/integration_test_summarizer.py +++ b/tests/integration_test_summarizer.py @@ -218,7 +218,7 @@ async def test_summarize_empty_message_buffer(server: SyncServer, actor, llm_con # Run summarization - this may fail with empty buffer, which is acceptable behavior try: - summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor) + _summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor) # If it succeeds, verify result assert isinstance(result, list) @@ -311,7 +311,7 @@ async def test_summarize_initialization_messages_only(server: SyncServer, actor, # Run summarization - force=True with system messages only may fail try: - summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor, force=True) + _summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor, force=True) # Verify result assert isinstance(result, list) @@ -367,7 +367,7 @@ async def test_summarize_small_conversation(server: SyncServer, actor, llm_confi # Run summarization with force=True # Note: force=True with clear=True can be very aggressive and may fail on small message sets try: - summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor, force=True) + _summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor, force=True) # Verify result assert isinstance(result, list) @@ -460,7 +460,7 @@ async def test_summarize_large_tool_calls(server: SyncServer, actor, llm_config: assert total_content_size > 40000, f"Expected large messages, got {total_content_size} chars" # Run summarization - summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor) + _summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor) # Verify result assert isinstance(result, list) @@ -564,7 +564,7 @@ async def test_summarize_multiple_large_tool_calls(server: SyncServer, actor, ll assert total_content_size > 40000, f"Expected large messages, got {total_content_size} chars" # Run summarization - summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor) + _summary, result, _ = await run_summarization(server, agent_state, in_context_messages, actor) # Verify result assert isinstance(result, list) @@ -724,7 +724,7 @@ async def test_summarize_with_mode(server: SyncServer, actor, llm_config: LLMCon agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor) - summary, result, summary_text = await agent_loop.compact(messages=in_context_messages) + _summary, result, summary_text = await agent_loop.compact(messages=in_context_messages) assert isinstance(result, list) @@ -810,7 +810,7 @@ async def test_compact_returns_valid_summary_message_and_event_message(server: S agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor) - summary_message_obj, compacted_messages, summary_text = await agent_loop.compact(messages=in_context_messages) + summary_message_obj, _compacted_messages, summary_text = await agent_loop.compact(messages=in_context_messages) # Verify we can construct a valid SummaryMessage from compact() return values summary_msg = SummaryMessage( @@ -971,7 +971,7 @@ async def test_v3_compact_uses_compaction_settings_model_and_model_settings(serv # Patch simple_summary so we don't hit the real LLM and can inspect llm_config with patch.object(summarizer_all, "simple_summary", new=fake_simple_summary): agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor) - summary_msg, compacted, _ = await agent_loop.compact(messages=in_context_messages) + summary_msg, _compacted, _ = await agent_loop.compact(messages=in_context_messages) assert summary_msg is not None assert "value" in captured_llm_config @@ -1059,7 +1059,7 @@ async def test_v3_summarize_hard_eviction_when_still_over_threshold( caplog.set_level("ERROR") - summary, result, summary_text = await agent_loop.compact( + _summary, result, summary_text = await agent_loop.compact( messages=in_context_messages, trigger_threshold=context_limit, ) @@ -2015,7 +2015,7 @@ async def test_compact_with_stats_params_embeds_stats(server: SyncServer, actor, agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor) # Call compact with stats params - summary_message_obj, compacted_messages, summary_text = await agent_loop.compact( + summary_message_obj, compacted_messages, _summary_text = await agent_loop.compact( messages=in_context_messages, use_summary_role=True, trigger="post_step_context_check", diff --git a/tests/integration_test_turbopuffer.py b/tests/integration_test_turbopuffer.py index ca90d2f4..d18874a4 100644 --- a/tests/integration_test_turbopuffer.py +++ b/tests/integration_test_turbopuffer.py @@ -45,7 +45,7 @@ async def sarah_agent(server, default_user): # Cleanup try: await server.agent_manager.delete_agent_async(agent.id, default_user) - except: + except Exception: pass @@ -151,7 +151,7 @@ async def wait_for_embedding( if any(msg["id"] == message_id for msg, _, _ in results): return True - except Exception as e: + except Exception: # Log but don't fail - Turbopuffer might still be processing pass @@ -347,7 +347,7 @@ async def test_turbopuffer_metadata_attributes(default_user, enable_turbopuffer) # Clean up on error try: await client.delete_all_passages(archive_id) - except: + except Exception: pass raise e @@ -409,7 +409,7 @@ async def test_hybrid_search_with_real_tpuf(default_user, enable_turbopuffer): ] # Create simple embeddings for testing (normally you'd use a real embedding model) - embeddings = [[float(i), float(i + 5), float(i + 10)] for i in range(len(texts))] + [[float(i), float(i + 5), float(i + 10)] for i in range(len(texts))] passage_ids = [f"passage-{str(uuid.uuid4())}" for _ in texts] # Insert passages @@ -487,7 +487,7 @@ async def test_hybrid_search_with_real_tpuf(default_user, enable_turbopuffer): # Clean up try: await client.delete_all_passages(archive_id) - except: + except Exception: pass @@ -522,7 +522,7 @@ async def test_tag_filtering_with_real_tpuf(default_user, enable_turbopuffer): ["javascript", "react"], ] - embeddings = [[float(i), float(i + 5), float(i + 10)] for i in range(len(texts))] + [[float(i), float(i + 5), float(i + 10)] for i in range(len(texts))] passage_ids = [f"passage-{str(uuid.uuid4())}" for _ in texts] # Insert passages with tags @@ -615,7 +615,7 @@ async def test_tag_filtering_with_real_tpuf(default_user, enable_turbopuffer): # Clean up try: await client.delete_all_passages(archive_id) - except: + except Exception: pass @@ -754,7 +754,7 @@ async def test_temporal_filtering_with_real_tpuf(default_user, enable_turbopuffe # Clean up try: await client.delete_all_passages(archive_id) - except: + except Exception: pass @@ -865,12 +865,6 @@ def test_message_text_extraction(server, default_user): agent_id="test-agent", ) text6 = manager._extract_message_text(msg6) - expected_parts = [ - "User said:", - 'Tool call: search({\n "query": "test"\n})', - "Tool result: Found 5 results", - "I should help the user", - ] assert ( text6 == '{"content": "User said: Tool call: search({\\n \\"query\\": \\"test\\"\\n}) Tool result: Found 5 results I should help the user"}' @@ -1112,7 +1106,7 @@ async def test_message_dual_write_with_real_tpuf(enable_message_embedding, defau created_ats = [datetime.now(timezone.utc) for _ in message_texts] # Generate embeddings (dummy for test) - embeddings = [[float(i), float(i + 1), float(i + 2)] for i in range(len(message_texts))] + [[float(i), float(i + 1), float(i + 2)] for i in range(len(message_texts))] # Insert messages into Turbopuffer success = await client.insert_messages( @@ -1144,7 +1138,7 @@ async def test_message_dual_write_with_real_tpuf(enable_message_embedding, defau # Clean up namespace try: await client.delete_all_messages(agent_id) - except: + except Exception: pass @@ -1205,7 +1199,7 @@ async def test_message_vector_search_with_real_tpuf(enable_message_embedding, de # Clean up namespace try: await client.delete_all_messages(agent_id) - except: + except Exception: pass @@ -1268,7 +1262,7 @@ async def test_message_hybrid_search_with_real_tpuf(enable_message_embedding, de # Clean up namespace try: await client.delete_all_messages(agent_id) - except: + except Exception: pass @@ -1340,7 +1334,7 @@ async def test_message_role_filtering_with_real_tpuf(enable_message_embedding, d # Clean up namespace try: await client.delete_all_messages(agent_id) - except: + except Exception: pass @@ -1357,7 +1351,7 @@ async def test_message_search_fallback_to_sql(server, default_user, sarah_agent) settings.embed_all_messages = False # Create messages - messages = await server.message_manager.create_many_messages_async( + await server.message_manager.create_many_messages_async( pydantic_msgs=[ PydanticMessage( role=MessageRole.user, @@ -1398,7 +1392,7 @@ async def test_message_update_reindexes_in_turbopuffer(server, default_user, sar """Test that updating a message properly deletes and re-inserts with new embedding in Turbopuffer""" from letta.schemas.message import MessageUpdate - embedding_config = sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") + sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") # Create initial message messages = await server.message_manager.create_many_messages_async( @@ -1493,8 +1487,6 @@ async def test_message_deletion_syncs_with_turbopuffer(server, default_user, ena actor=default_user, ) - embedding_config = agent_a.embedding_config - try: # Create 5 messages for agent A agent_a_messages = [] @@ -1597,7 +1589,7 @@ async def test_turbopuffer_failure_does_not_break_postgres(server, default_user, from letta.schemas.message import MessageUpdate - embedding_config = sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") + sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") # Create initial messages messages = await server.message_manager.create_many_messages_async( @@ -1668,7 +1660,7 @@ async def test_turbopuffer_failure_does_not_break_postgres(server, default_user, @pytest.mark.skipif(not settings.tpuf_api_key, reason="Turbopuffer API key not configured") async def test_message_creation_background_mode(server, default_user, sarah_agent, enable_message_embedding): """Test that messages are embedded in background when strict_mode=False""" - embedding_config = sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") + sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") # Create message in background mode messages = await server.message_manager.create_many_messages_async( @@ -1723,7 +1715,7 @@ async def test_message_update_background_mode(server, default_user, sarah_agent, """Test that message updates work in background mode""" from letta.schemas.message import MessageUpdate - embedding_config = sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") + sarah_agent.embedding_config or EmbeddingConfig.default_config(provider="openai") # Create initial message with strict_mode=True to ensure it's embedded messages = await server.message_manager.create_many_messages_async( @@ -1899,7 +1891,7 @@ async def test_message_date_filtering_with_real_tpuf(enable_message_embedding, d # Clean up namespace try: await client.delete_all_messages(agent_id) - except: + except Exception: pass @@ -2403,7 +2395,7 @@ async def test_query_messages_with_mixed_conversation_id_presence(enable_message async with AsyncTurbopuffer(api_key=client.api_key, region=client.region) as tpuf: namespace = tpuf.namespace(namespace_name) await namespace.delete_all() - except: + except Exception: pass @@ -2485,7 +2477,7 @@ async def test_query_messages_by_org_id_with_missing_conversation_id_schema(enab async with AsyncTurbopuffer(api_key=client.api_key, region=client.region) as tpuf: namespace = tpuf.namespace(namespace_name) await namespace.delete_all() - except: + except Exception: pass @@ -2541,5 +2533,5 @@ async def test_system_messages_not_embedded_during_agent_creation(server, defaul # Clean up try: await server.agent_manager.delete_agent_async(agent.id, default_user) - except: + except Exception: pass diff --git a/tests/integration_test_usage_tracking.py b/tests/integration_test_usage_tracking.py index c6887da9..c010690e 100644 --- a/tests/integration_test_usage_tracking.py +++ b/tests/integration_test_usage_tracking.py @@ -400,7 +400,7 @@ async def test_run_level_usage_aggregation( try: # Send multiple messages to create multiple steps - response1: Run = await async_client.agents.messages.send_message( + await async_client.agents.messages.send_message( agent_id=agent.id, messages=[MessageCreateParam(role="user", content="Message 1")], ) diff --git a/tests/managers/conftest.py b/tests/managers/conftest.py index f6dcf9ac..6fbd4c79 100644 --- a/tests/managers/conftest.py +++ b/tests/managers/conftest.py @@ -497,7 +497,7 @@ async def sandbox_env_var_fixture(server: SyncServer, sandbox_config_fixture, de @pytest.fixture async def file_attachment(server: SyncServer, default_user, sarah_agent, default_file): """Create a file attachment to an agent.""" - assoc, closed_files = await server.file_agent_manager.attach_file( + assoc, _closed_files = await server.file_agent_manager.attach_file( agent_id=sarah_agent.id, file_id=default_file.id, file_name=default_file.file_name, diff --git a/tests/managers/test_agent_manager.py b/tests/managers/test_agent_manager.py index 0eaedc05..07d24289 100644 --- a/tests/managers/test_agent_manager.py +++ b/tests/managers/test_agent_manager.py @@ -279,7 +279,7 @@ async def test_compaction_settings_model_uses_separate_llm_config_for_summarizat ) # Minimal message buffer: system + one user + one assistant - messages = [ + [ PydanticMessage( role=MessageRole.system, content=[TextContent(type="text", text="You are a helpful assistant.")], @@ -500,10 +500,10 @@ async def test_get_context_window_basic( server: SyncServer, comprehensive_test_agent_fixture, default_user, default_file, set_letta_environment ): # Test agent creation - created_agent, create_agent_request = comprehensive_test_agent_fixture + created_agent, _create_agent_request = comprehensive_test_agent_fixture # Attach a file - assoc, closed_files = await server.file_agent_manager.attach_file( + assoc, _closed_files = await server.file_agent_manager.attach_file( agent_id=created_agent.id, file_id=default_file.id, file_name=default_file.file_name, @@ -879,7 +879,7 @@ async def test_update_agent_last_stop_reason(server: SyncServer, comprehensive_t @pytest.mark.asyncio async def test_list_agents_select_fields_empty(server: SyncServer, comprehensive_test_agent_fixture, default_user): # Create an agent using the comprehensive fixture. - created_agent, create_agent_request = comprehensive_test_agent_fixture + _created_agent, _create_agent_request = comprehensive_test_agent_fixture # List agents using an empty list for select_fields. agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=[]) @@ -897,7 +897,7 @@ async def test_list_agents_select_fields_empty(server: SyncServer, comprehensive @pytest.mark.asyncio async def test_list_agents_select_fields_none(server: SyncServer, comprehensive_test_agent_fixture, default_user): # Create an agent using the comprehensive fixture. - created_agent, create_agent_request = comprehensive_test_agent_fixture + _created_agent, _create_agent_request = comprehensive_test_agent_fixture # List agents using an empty list for select_fields. agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=None) @@ -914,7 +914,7 @@ async def test_list_agents_select_fields_none(server: SyncServer, comprehensive_ @pytest.mark.asyncio async def test_list_agents_select_fields_specific(server: SyncServer, comprehensive_test_agent_fixture, default_user): - created_agent, create_agent_request = comprehensive_test_agent_fixture + _created_agent, _create_agent_request = comprehensive_test_agent_fixture # Choose a subset of valid relationship fields. valid_fields = ["tools", "tags"] @@ -931,7 +931,7 @@ async def test_list_agents_select_fields_specific(server: SyncServer, comprehens @pytest.mark.asyncio async def test_list_agents_select_fields_invalid(server: SyncServer, comprehensive_test_agent_fixture, default_user): - created_agent, create_agent_request = comprehensive_test_agent_fixture + _created_agent, _create_agent_request = comprehensive_test_agent_fixture # Provide field names that are not recognized. invalid_fields = ["foobar", "nonexistent_field"] @@ -946,7 +946,7 @@ async def test_list_agents_select_fields_invalid(server: SyncServer, comprehensi @pytest.mark.asyncio async def test_list_agents_select_fields_duplicates(server: SyncServer, comprehensive_test_agent_fixture, default_user): - created_agent, create_agent_request = comprehensive_test_agent_fixture + _created_agent, _create_agent_request = comprehensive_test_agent_fixture # Provide duplicate valid field names. duplicate_fields = ["tools", "tools", "tags", "tags"] @@ -961,7 +961,7 @@ async def test_list_agents_select_fields_duplicates(server: SyncServer, comprehe @pytest.mark.asyncio async def test_list_agents_select_fields_mixed(server: SyncServer, comprehensive_test_agent_fixture, default_user): - created_agent, create_agent_request = comprehensive_test_agent_fixture + _created_agent, _create_agent_request = comprehensive_test_agent_fixture # Mix valid fields with an invalid one. mixed_fields = ["tools", "invalid_field"] @@ -978,7 +978,7 @@ async def test_list_agents_select_fields_mixed(server: SyncServer, comprehensive @pytest.mark.asyncio async def test_list_agents_ascending(server: SyncServer, default_user): # Create two agents with known names - agent1 = await server.agent_manager.create_agent_async( + await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_oldest", agent_type="memgpt_v2_agent", @@ -993,7 +993,7 @@ async def test_list_agents_ascending(server: SyncServer, default_user): if USING_SQLITE: time.sleep(CREATE_DELAY_SQLITE) - agent2 = await server.agent_manager.create_agent_async( + await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_newest", agent_type="memgpt_v2_agent", @@ -1013,7 +1013,7 @@ async def test_list_agents_ascending(server: SyncServer, default_user): @pytest.mark.asyncio async def test_list_agents_descending(server: SyncServer, default_user): # Create two agents with known names - agent1 = await server.agent_manager.create_agent_async( + await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_oldest", agent_type="memgpt_v2_agent", @@ -1028,7 +1028,7 @@ async def test_list_agents_descending(server: SyncServer, default_user): if USING_SQLITE: time.sleep(CREATE_DELAY_SQLITE) - agent2 = await server.agent_manager.create_agent_async( + await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_newest", agent_type="memgpt_v2_agent", @@ -1084,7 +1084,7 @@ async def test_list_agents_by_last_stop_reason(server: SyncServer, default_user) ) # Create agent with no stop reason - agent3 = await server.agent_manager.create_agent_async( + await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_no_stop_reason", agent_type="memgpt_v2_agent", @@ -1172,7 +1172,7 @@ async def test_count_agents_with_filters(server: SyncServer, default_user): actor=default_user, ) - agent4 = await server.agent_manager.create_agent_async( + await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_no_stop_reason", agent_type="memgpt_v2_agent", @@ -1963,14 +1963,14 @@ async def test_create_template_agent_with_files_from_sources(server: SyncServer, organization_id=default_user.organization_id, source_id=source.id, ) - file1 = await server.file_manager.create_file(file_metadata=file1_metadata, actor=default_user, text="content for file 1") + await server.file_manager.create_file(file_metadata=file1_metadata, actor=default_user, text="content for file 1") file2_metadata = PydanticFileMetadata( file_name="template_file_2.txt", organization_id=default_user.organization_id, source_id=source.id, ) - file2 = await server.file_manager.create_file(file_metadata=file2_metadata, actor=default_user, text="content for file 2") + await server.file_manager.create_file(file_metadata=file2_metadata, actor=default_user, text="content for file 2") # Create agent using InternalTemplateAgentCreate with the source create_agent_request = InternalTemplateAgentCreate( diff --git a/tests/managers/test_cancellation.py b/tests/managers/test_cancellation.py index e154150d..1aca3f8c 100644 --- a/tests/managers/test_cancellation.py +++ b/tests/managers/test_cancellation.py @@ -320,7 +320,7 @@ class TestMessageStateDesyncIssues: print(f" background={request.background}") # Start the background streaming agent - run, stream_response = await streaming_service.create_agent_stream( + run, _stream_response = await streaming_service.create_agent_stream( agent_id=test_agent_with_tool.id, actor=default_user, request=request, @@ -510,7 +510,7 @@ class TestStreamingCancellation: try: async for chunk in cancel_during_stream(): chunks.append(chunk) - except Exception as e: + except Exception: # May raise exception on cancellation pass @@ -733,7 +733,7 @@ class TestResourceCleanupAfterCancellation: input_messages = [MessageCreate(role=MessageRole.user, content="Call print_tool with 'test'")] - result = await agent_loop.step( + await agent_loop.step( input_messages=input_messages, max_steps=5, run_id=test_run.id, @@ -895,7 +895,7 @@ class TestApprovalFlowCancellation: ) # Check for approval request messages - approval_messages = [m for m in messages_after_cancel if m.role == "approval_request"] + [m for m in messages_after_cancel if m.role == "approval_request"] # Second run: try to execute normally (should work, not stuck in approval) test_run_2 = await server.run_manager.create_run( @@ -1075,7 +1075,7 @@ class TestApprovalFlowCancellation: assert result.stop_reason.stop_reason == "requires_approval", f"Expected requires_approval, got {result.stop_reason.stop_reason}" # Get all messages from database for this run - db_messages = await server.message_manager.list_messages( + await server.message_manager.list_messages( actor=default_user, agent_id=test_agent_with_tool.id, run_id=test_run.id, @@ -1210,7 +1210,7 @@ class TestApprovalFlowCancellation: assert result.stop_reason.stop_reason == "requires_approval", f"Should stop for approval, got {result.stop_reason.stop_reason}" # Get the approval request message to see how many tool calls were made - db_messages_before_cancel = await server.message_manager.list_messages( + await server.message_manager.list_messages( actor=default_user, agent_id=agent_state.id, run_id=test_run.id, diff --git a/tests/managers/test_file_manager.py b/tests/managers/test_file_manager.py index e77fbe75..79760a12 100644 --- a/tests/managers/test_file_manager.py +++ b/tests/managers/test_file_manager.py @@ -18,7 +18,7 @@ from letta.schemas.file import FileMetadata as PydanticFileMetadata @pytest.mark.asyncio async def test_attach_creates_association(server, default_user, sarah_agent, default_file): - assoc, closed_files = await server.file_agent_manager.attach_file( + assoc, _closed_files = await server.file_agent_manager.attach_file( agent_id=sarah_agent.id, file_id=default_file.id, file_name=default_file.file_name, @@ -40,7 +40,7 @@ async def test_attach_creates_association(server, default_user, sarah_agent, def async def test_attach_is_idempotent(server, default_user, sarah_agent, default_file): - a1, closed_files = await server.file_agent_manager.attach_file( + a1, _closed_files = await server.file_agent_manager.attach_file( agent_id=sarah_agent.id, file_id=default_file.id, file_name=default_file.file_name, @@ -51,7 +51,7 @@ async def test_attach_is_idempotent(server, default_user, sarah_agent, default_f ) # second attach with different params - a2, closed_files = await server.file_agent_manager.attach_file( + a2, _closed_files = await server.file_agent_manager.attach_file( agent_id=sarah_agent.id, file_id=default_file.id, file_name=default_file.file_name, @@ -114,7 +114,7 @@ async def test_file_agent_line_tracking(server, default_user, sarah_agent, defau file = await server.file_manager.create_file(file_metadata=file_metadata, actor=default_user, text=test_content) # Test opening with line range using enforce_max_open_files_and_open - closed_files, was_already_open, previous_ranges = await server.file_agent_manager.enforce_max_open_files_and_open( + _closed_files, _was_already_open, previous_ranges = await server.file_agent_manager.enforce_max_open_files_and_open( agent_id=sarah_agent.id, file_id=file.id, file_name=file.file_name, @@ -138,7 +138,7 @@ async def test_file_agent_line_tracking(server, default_user, sarah_agent, defau assert previous_ranges == {} # No previous range since it wasn't open before # Test opening without line range - should clear line info and capture previous range - closed_files, was_already_open, previous_ranges = await server.file_agent_manager.enforce_max_open_files_and_open( + _closed_files, _was_already_open, previous_ranges = await server.file_agent_manager.enforce_max_open_files_and_open( agent_id=sarah_agent.id, file_id=file.id, file_name=file.file_name, @@ -321,7 +321,7 @@ async def test_list_files_for_agent_paginated_filter_open( ) # get only open files - open_files, cursor, has_more = await server.file_agent_manager.list_files_for_agent_paginated( + open_files, _cursor, has_more = await server.file_agent_manager.list_files_for_agent_paginated( agent_id=sarah_agent.id, actor=default_user, is_open=True, @@ -370,7 +370,7 @@ async def test_list_files_for_agent_paginated_filter_closed( assert all(not fa.is_open for fa in page1) # get second page of closed files - page2, cursor2, has_more2 = await server.file_agent_manager.list_files_for_agent_paginated( + page2, _cursor2, has_more2 = await server.file_agent_manager.list_files_for_agent_paginated( agent_id=sarah_agent.id, actor=default_user, is_open=False, @@ -586,7 +586,7 @@ async def test_mark_access_bulk(server, default_user, sarah_agent, default_sourc # Attach all files (they'll be open by default) attached_files = [] for file in files: - file_agent, closed_files = await server.file_agent_manager.attach_file( + file_agent, _closed_files = await server.file_agent_manager.attach_file( agent_id=sarah_agent.id, file_id=file.id, file_name=file.file_name, @@ -745,7 +745,7 @@ async def test_lru_eviction_on_open_file(server, default_user, sarah_agent, defa time.sleep(0.1) # Now "open" the last file using the efficient method - closed_files, was_already_open, _ = await server.file_agent_manager.enforce_max_open_files_and_open( + closed_files, _was_already_open, _ = await server.file_agent_manager.enforce_max_open_files_and_open( agent_id=sarah_agent.id, file_id=files[-1].id, file_name=files[-1].file_name, @@ -853,7 +853,7 @@ async def test_last_accessed_at_updates_correctly(server, default_user, sarah_ag ) file = await server.file_manager.create_file(file_metadata=file_metadata, actor=default_user, text="test content") - file_agent, closed_files = await server.file_agent_manager.attach_file( + file_agent, _closed_files = await server.file_agent_manager.attach_file( agent_id=sarah_agent.id, file_id=file.id, file_name=file.file_name, @@ -957,7 +957,7 @@ async def test_attach_files_bulk_deduplication(server, default_user, sarah_agent visible_content_map = {"duplicate_test.txt": "visible content"} # Bulk attach should deduplicate - closed_files = await server.file_agent_manager.attach_files_bulk( + await server.file_agent_manager.attach_files_bulk( agent_id=sarah_agent.id, files_metadata=files_to_attach, visible_content_map=visible_content_map, @@ -1085,7 +1085,7 @@ async def test_attach_files_bulk_mixed_existing_new(server, default_user, sarah_ new_files.append(file) # Bulk attach: existing file + new files - files_to_attach = [existing_file] + new_files + files_to_attach = [existing_file, *new_files] visible_content_map = { "existing_file.txt": "updated content", "new_file_0.txt": "new content 0", diff --git a/tests/managers/test_identity_manager.py b/tests/managers/test_identity_manager.py index 278cc151..beca8049 100644 --- a/tests/managers/test_identity_manager.py +++ b/tests/managers/test_identity_manager.py @@ -306,31 +306,3 @@ async def test_get_set_blocks_for_identities(server: SyncServer, default_block, assert block_without_identity.id not in block_ids await server.identity_manager.delete_identity_async(identity_id=identity.id, actor=default_user) - - -async def test_upsert_properties(server: SyncServer, default_user): - identity_create = IdentityCreate( - identifier_key="1234", - name="caren", - identity_type=IdentityType.user, - properties=[ - IdentityProperty(key="email", value="caren@letta.com", type=IdentityPropertyType.string), - IdentityProperty(key="age", value=28, type=IdentityPropertyType.number), - ], - ) - - identity = await server.identity_manager.create_identity_async(identity_create, actor=default_user) - properties = [ - IdentityProperty(key="email", value="caren@gmail.com", type=IdentityPropertyType.string), - IdentityProperty(key="age", value="28", type=IdentityPropertyType.string), - IdentityProperty(key="test", value=123, type=IdentityPropertyType.number), - ] - - updated_identity = await server.identity_manager.upsert_identity_properties_async( - identity_id=identity.id, - properties=properties, - actor=default_user, - ) - assert updated_identity.properties == properties - - await server.identity_manager.delete_identity_async(identity_id=identity.id, actor=default_user) diff --git a/tests/managers/test_mcp_manager.py b/tests/managers/test_mcp_manager.py index 9eab0dc3..fb27404b 100644 --- a/tests/managers/test_mcp_manager.py +++ b/tests/managers/test_mcp_manager.py @@ -71,7 +71,7 @@ async def test_create_mcp_server(mock_get_client, server, default_user): # Test with a valid SSEServerConfig mcp_server_name = "coingecko" server_url = "https://mcp.api.coingecko.com/sse" - sse_mcp_config = SSEServerConfig(server_name=mcp_server_name, server_url=server_url) + SSEServerConfig(server_name=mcp_server_name, server_url=server_url) mcp_sse_server = MCPServer(server_name=mcp_server_name, server_type=MCPServerType.SSE, server_url=server_url) created_server = await server.mcp_manager.create_or_update_mcp_server(mcp_sse_server, actor=default_user) print(created_server) @@ -797,7 +797,7 @@ async def test_mcp_server_resync_tools(server, default_user, default_organizatio # Verify tool2 was actually deleted try: - deleted_tool = await server.tool_manager.get_tool_by_id_async(tool_id=tool2.id, actor=default_user) + await server.tool_manager.get_tool_by_id_async(tool_id=tool2.id, actor=default_user) assert False, "Tool2 should have been deleted" except Exception: pass # Expected - tool should be deleted diff --git a/tests/managers/test_message_manager.py b/tests/managers/test_message_manager.py index 593c64d4..6a689ec7 100644 --- a/tests/managers/test_message_manager.py +++ b/tests/managers/test_message_manager.py @@ -214,10 +214,10 @@ async def test_modify_letta_message(server: SyncServer, sarah_agent, default_use messages = await server.message_manager.list_messages(agent_id=sarah_agent.id, actor=default_user) letta_messages = PydanticMessage.to_letta_messages_from_list(messages=messages) - system_message = [msg for msg in letta_messages if msg.message_type == "system_message"][0] - assistant_message = [msg for msg in letta_messages if msg.message_type == "assistant_message"][0] - user_message = [msg for msg in letta_messages if msg.message_type == "user_message"][0] - reasoning_message = [msg for msg in letta_messages if msg.message_type == "reasoning_message"][0] + system_message = next(msg for msg in letta_messages if msg.message_type == "system_message") + assistant_message = next(msg for msg in letta_messages if msg.message_type == "assistant_message") + user_message = next(msg for msg in letta_messages if msg.message_type == "user_message") + reasoning_message = next(msg for msg in letta_messages if msg.message_type == "reasoning_message") # user message update_user_message = UpdateUserMessage(content="Hello, Sarah!") diff --git a/tests/managers/test_provider_manager.py b/tests/managers/test_provider_manager.py index d6ae398a..183417cc 100644 --- a/tests/managers/test_provider_manager.py +++ b/tests/managers/test_provider_manager.py @@ -908,7 +908,7 @@ async def test_server_startup_handles_api_errors_gracefully(default_user, defaul actor=default_user, ) if len(openai_providers) > 0: - openai_models = await server.provider_manager.list_models_async( + await server.provider_manager.list_models_async( actor=default_user, provider_id=openai_providers[0].id, ) diff --git a/tests/managers/test_run_manager.py b/tests/managers/test_run_manager.py index 94934799..1167c19d 100644 --- a/tests/managers/test_run_manager.py +++ b/tests/managers/test_run_manager.py @@ -161,8 +161,7 @@ async def test_update_run_updates_agent_last_stop_reason(server: SyncServer, sar """Test that completing a run updates the agent's last_stop_reason.""" # Verify agent starts with no last_stop_reason - agent = await server.agent_manager.get_agent_by_id_async(agent_id=sarah_agent.id, actor=default_user) - initial_stop_reason = agent.last_stop_reason + await server.agent_manager.get_agent_by_id_async(agent_id=sarah_agent.id, actor=default_user) # Create a run run_data = PydanticRun(agent_id=sarah_agent.id) @@ -867,7 +866,7 @@ async def test_run_messages_ordering(server: SyncServer, default_run, default_us created_at=created_at, run_id=run.id, ) - msg = await server.message_manager.create_many_messages_async([message], actor=default_user) + await server.message_manager.create_many_messages_async([message], actor=default_user) # Verify messages are returned in chronological order returned_messages = await server.message_manager.list_messages( @@ -1015,7 +1014,7 @@ async def test_get_run_messages(server: SyncServer, default_user: PydanticUser, ) ) - created_msg = await server.message_manager.create_many_messages_async(messages, actor=default_user) + await server.message_manager.create_many_messages_async(messages, actor=default_user) # Get messages and verify they're converted correctly result = await server.message_manager.list_messages(run_id=run.id, actor=default_user) @@ -1088,7 +1087,7 @@ async def test_get_run_messages_with_assistant_message(server: SyncServer, defau ) ) - created_msg = await server.message_manager.create_many_messages_async(messages, actor=default_user) + await server.message_manager.create_many_messages_async(messages, actor=default_user) # Get messages and verify they're converted correctly result = await server.message_manager.list_messages(run_id=run.id, actor=default_user) @@ -1369,7 +1368,7 @@ async def test_run_metrics_duration_calculation(server: SyncServer, sarah_agent, await asyncio.sleep(0.1) # Wait 100ms # Update the run to completed - updated_run = await server.run_manager.update_run_by_id_async( + await server.run_manager.update_run_by_id_async( created_run.id, RunUpdate(status=RunStatus.completed, stop_reason=StopReasonType.end_turn), actor=default_user ) @@ -1663,7 +1662,7 @@ def test_convert_statuses_to_enum_with_invalid_status(): async def test_list_runs_with_multiple_statuses(server: SyncServer, sarah_agent, default_user): """Test listing runs with multiple status filters.""" # Create runs with different statuses - run_created = await server.run_manager.create_run( + await server.run_manager.create_run( pydantic_run=PydanticRun( status=RunStatus.created, agent_id=sarah_agent.id, @@ -1671,7 +1670,7 @@ async def test_list_runs_with_multiple_statuses(server: SyncServer, sarah_agent, ), actor=default_user, ) - run_running = await server.run_manager.create_run( + await server.run_manager.create_run( pydantic_run=PydanticRun( status=RunStatus.running, agent_id=sarah_agent.id, @@ -1679,7 +1678,7 @@ async def test_list_runs_with_multiple_statuses(server: SyncServer, sarah_agent, ), actor=default_user, ) - run_completed = await server.run_manager.create_run( + await server.run_manager.create_run( pydantic_run=PydanticRun( status=RunStatus.completed, agent_id=sarah_agent.id, @@ -1687,7 +1686,7 @@ async def test_list_runs_with_multiple_statuses(server: SyncServer, sarah_agent, ), actor=default_user, ) - run_failed = await server.run_manager.create_run( + await server.run_manager.create_run( pydantic_run=PydanticRun( status=RunStatus.failed, agent_id=sarah_agent.id, diff --git a/tests/managers/test_source_manager.py b/tests/managers/test_source_manager.py index 05af9959..a7e032da 100644 --- a/tests/managers/test_source_manager.py +++ b/tests/managers/test_source_manager.py @@ -387,7 +387,7 @@ async def test_create_sources_with_same_name_raises_error(server: SyncServer, de metadata={"type": "medical"}, embedding_config=DEFAULT_EMBEDDING_CONFIG, ) - source = await server.source_manager.create_source(source=source_pydantic, actor=default_user) + await server.source_manager.create_source(source=source_pydantic, actor=default_user) # Attempting to create another source with the same name should raise an IntegrityError source_pydantic = PydanticSource( @@ -1120,7 +1120,7 @@ async def test_file_status_invalid_transitions(server, default_user, default_sou ) created = await server.file_manager.create_file(file_metadata=meta, actor=default_user) - with pytest.raises(ValueError, match="Invalid state transition.*pending.*COMPLETED"): + with pytest.raises(ValueError, match=r"Invalid state transition.*pending.*COMPLETED"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1142,7 +1142,7 @@ async def test_file_status_invalid_transitions(server, default_user, default_sou processing_status=FileProcessingStatus.PARSING, ) - with pytest.raises(ValueError, match="Invalid state transition.*parsing.*COMPLETED"): + with pytest.raises(ValueError, match=r"Invalid state transition.*parsing.*COMPLETED"): await server.file_manager.update_file_status( file_id=created2.id, actor=default_user, @@ -1159,7 +1159,7 @@ async def test_file_status_invalid_transitions(server, default_user, default_sou ) created3 = await server.file_manager.create_file(file_metadata=meta3, actor=default_user) - with pytest.raises(ValueError, match="Invalid state transition.*pending.*EMBEDDING"): + with pytest.raises(ValueError, match=r"Invalid state transition.*pending.*EMBEDDING"): await server.file_manager.update_file_status( file_id=created3.id, actor=default_user, @@ -1186,14 +1186,14 @@ async def test_file_status_terminal_states(server, default_user, default_source) await server.file_manager.update_file_status(file_id=created.id, actor=default_user, processing_status=FileProcessingStatus.COMPLETED) # Cannot transition from COMPLETED to any state - with pytest.raises(ValueError, match="Cannot update.*terminal state completed"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state completed"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, processing_status=FileProcessingStatus.EMBEDDING, ) - with pytest.raises(ValueError, match="Cannot update.*terminal state completed"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state completed"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1219,7 +1219,7 @@ async def test_file_status_terminal_states(server, default_user, default_source) ) # Cannot transition from ERROR to any state - with pytest.raises(ValueError, match="Cannot update.*terminal state error"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state error"): await server.file_manager.update_file_status( file_id=created2.id, actor=default_user, @@ -1313,7 +1313,7 @@ async def test_file_status_terminal_state_non_status_updates(server, default_use await server.file_manager.update_file_status(file_id=created.id, actor=default_user, processing_status=FileProcessingStatus.COMPLETED) # Cannot update chunks_embedded in COMPLETED state - with pytest.raises(ValueError, match="Cannot update.*terminal state completed"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state completed"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1321,7 +1321,7 @@ async def test_file_status_terminal_state_non_status_updates(server, default_use ) # Cannot update total_chunks in COMPLETED state - with pytest.raises(ValueError, match="Cannot update.*terminal state completed"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state completed"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1329,7 +1329,7 @@ async def test_file_status_terminal_state_non_status_updates(server, default_use ) # Cannot update error_message in COMPLETED state - with pytest.raises(ValueError, match="Cannot update.*terminal state completed"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state completed"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1353,7 +1353,7 @@ async def test_file_status_terminal_state_non_status_updates(server, default_use ) # Cannot update chunks_embedded in ERROR state - with pytest.raises(ValueError, match="Cannot update.*terminal state error"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state error"): await server.file_manager.update_file_status( file_id=created2.id, actor=default_user, @@ -1399,7 +1399,7 @@ async def test_file_status_race_condition_prevention(server, default_user, defau # Try to continue with EMBEDDING as if error didn't happen (race condition) # This should fail because file is in ERROR state - with pytest.raises(ValueError, match="Cannot update.*terminal state error"): + with pytest.raises(ValueError, match=r"Cannot update.*terminal state error"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1424,7 +1424,7 @@ async def test_file_status_backwards_transitions(server, default_user, default_s await server.file_manager.update_file_status(file_id=created.id, actor=default_user, processing_status=FileProcessingStatus.EMBEDDING) # Cannot go back to PARSING - with pytest.raises(ValueError, match="Invalid state transition.*embedding.*PARSING"): + with pytest.raises(ValueError, match=r"Invalid state transition.*embedding.*PARSING"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, @@ -1432,7 +1432,7 @@ async def test_file_status_backwards_transitions(server, default_user, default_s ) # Cannot go back to PENDING - with pytest.raises(ValueError, match="Cannot transition to PENDING state.*PENDING is only valid as initial state"): + with pytest.raises(ValueError, match=r"Cannot transition to PENDING state.*PENDING is only valid as initial state"): await server.file_manager.update_file_status( file_id=created.id, actor=default_user, diff --git a/tests/managers/test_tool_manager.py b/tests/managers/test_tool_manager.py index 68a09541..4489d68e 100644 --- a/tests/managers/test_tool_manager.py +++ b/tests/managers/test_tool_manager.py @@ -1945,8 +1945,8 @@ def test_function(): source_code=source_code, ) - with pytest.raises(ValueError) as exc_info: - created_tool = await tool_manager.create_or_update_tool_async(tool, default_user) + with pytest.raises(ValueError): + await tool_manager.create_or_update_tool_async(tool, default_user) async def test_error_on_create_tool_with_name_conflict(server: SyncServer, default_user, default_organization): diff --git a/tests/manual_test_many_messages.py b/tests/manual_test_many_messages.py index 795515ad..bafeb3b4 100644 --- a/tests/manual_test_many_messages.py +++ b/tests/manual_test_many_messages.py @@ -17,7 +17,7 @@ from letta.server.server import SyncServer @pytest.fixture(autouse=True) def truncate_database(): - from letta.server.db import db_context + from letta.server.db import db_context # type: ignore[attr-defined] with db_context() as session: for table in reversed(Base.metadata.sorted_tables): # Reverse to avoid FK issues diff --git a/tests/mcp_tests/test_schema_validator.py b/tests/mcp_tests/test_schema_validator.py index 80d93ab2..41eba96e 100644 --- a/tests/mcp_tests/test_schema_validator.py +++ b/tests/mcp_tests/test_schema_validator.py @@ -186,7 +186,7 @@ class TestSchemaValidator: } # This should actually be STRICT_COMPLIANT since empty arrays with defined items are OK - status, reasons = validate_complete_json_schema(schema) + status, _reasons = validate_complete_json_schema(schema) assert status == SchemaHealth.STRICT_COMPLIANT def test_array_without_constraints_invalid(self): diff --git a/tests/performance_tests/test_insert_archival_memory.py b/tests/performance_tests/test_insert_archival_memory.py index 93deedce..04ac2406 100644 --- a/tests/performance_tests/test_insert_archival_memory.py +++ b/tests/performance_tests/test_insert_archival_memory.py @@ -111,7 +111,7 @@ async def test_insert_archival_memories_concurrent(client): cdf_y = np.arange(1, len(durs_sorted) + 1) / len(durs_sorted) # Plot all 6 subplots - fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + _fig, axes = plt.subplots(2, 3, figsize=(15, 8)) axs = axes.ravel() # 1) Kickoff timeline diff --git a/tests/sdk/mcp_servers_test.py b/tests/sdk/mcp_servers_test.py index 4fc0a816..94efdec1 100644 --- a/tests/sdk/mcp_servers_test.py +++ b/tests/sdk/mcp_servers_test.py @@ -187,7 +187,7 @@ def get_attr(obj, attr): return getattr(obj, attr, None) -def create_stdio_server_request(server_name: str, command: str = "npx", args: List[str] = None) -> Dict[str, Any]: +def create_stdio_server_request(server_name: str, command: str = "npx", args: List[str] | None = None) -> Dict[str, Any]: """Create a stdio MCP server configuration object. Returns a dict with server_name and config following CreateMCPServerRequest schema. @@ -203,7 +203,7 @@ def create_stdio_server_request(server_name: str, command: str = "npx", args: Li } -def create_sse_server_request(server_name: str, server_url: str = None) -> Dict[str, Any]: +def create_sse_server_request(server_name: str, server_url: str | None = None) -> Dict[str, Any]: """Create an SSE MCP server configuration object. Returns a dict with server_name and config following CreateMCPServerRequest schema. @@ -220,7 +220,7 @@ def create_sse_server_request(server_name: str, server_url: str = None) -> Dict[ } -def create_streamable_http_server_request(server_name: str, server_url: str = None) -> Dict[str, Any]: +def create_streamable_http_server_request(server_name: str, server_url: str | None = None) -> Dict[str, Any]: """Create a streamable HTTP MCP server configuration object. Returns a dict with server_name and config following CreateMCPServerRequest schema. @@ -508,7 +508,7 @@ def test_invalid_server_type(client: Letta): client.mcp_servers.create(**invalid_config) # If we get here without an exception, the test should fail assert False, "Expected an error when creating server with missing required fields" - except (BadRequestError, UnprocessableEntityError, TypeError, ValueError) as e: + except (BadRequestError, UnprocessableEntityError, TypeError, ValueError): # Expected to fail - this is good test_passed = True diff --git a/tests/sdk/search_test.py b/tests/sdk/search_test.py index aec2e169..ac9946ba 100644 --- a/tests/sdk/search_test.py +++ b/tests/sdk/search_test.py @@ -220,7 +220,7 @@ def test_passage_search_basic(client: Letta, enable_turbopuffer): # Clean up archive try: client.archives.delete(archive_id=archive.id) - except: + except Exception: pass finally: @@ -282,7 +282,7 @@ def test_passage_search_with_tags(client: Letta, enable_turbopuffer): # Clean up archive try: client.archives.delete(archive_id=archive.id) - except: + except Exception: pass finally: @@ -350,7 +350,7 @@ def test_passage_search_with_date_filters(client: Letta, enable_turbopuffer): # Clean up archive try: client.archives.delete(archive_id=archive.id) - except: + except Exception: pass finally: @@ -489,7 +489,7 @@ def test_passage_search_pagination(client: Letta, enable_turbopuffer): # Clean up archive try: client.archives.delete(archive_id=archive.id) - except: + except Exception: pass finally: @@ -554,11 +554,11 @@ def test_passage_search_org_wide(client: Letta, enable_turbopuffer): # Clean up archives try: client.archives.delete(archive_id=archive1.id) - except: + except Exception: pass try: client.archives.delete(archive_id=archive2.id) - except: + except Exception: pass finally: diff --git a/tests/test_agent_serialization.py b/tests/test_agent_serialization.py index 5213076f..017eb00d 100644 --- a/tests/test_agent_serialization.py +++ b/tests/test_agent_serialization.py @@ -355,7 +355,7 @@ def compare_in_context_message_id_remapping(server, og_agent: AgentState, copy_a remapped IDs but identical relevant content and order. """ # Serialize the original agent state - result = server.agent_manager.serialize(agent_id=og_agent.id, actor=og_user) + server.agent_manager.serialize(agent_id=og_agent.id, actor=og_user) # Retrieve the in-context messages for both the original and the copy # Corrected typo: agent_id instead of agent_id diff --git a/tests/test_agent_serialization_v2.py b/tests/test_agent_serialization_v2.py index 8bc6f21b..29793c77 100644 --- a/tests/test_agent_serialization_v2.py +++ b/tests/test_agent_serialization_v2.py @@ -774,7 +774,7 @@ class TestFileExport: @pytest.mark.asyncio async def test_basic_file_export(self, default_user, agent_serialization_manager, agent_with_files): """Test basic file export functionality""" - agent_id, source_id, file_id = agent_with_files + agent_id, _source_id, _file_id = agent_with_files exported = await agent_serialization_manager.export([agent_id], actor=default_user) @@ -925,7 +925,7 @@ class TestFileExport: @pytest.mark.asyncio async def test_file_content_inclusion_in_export(self, default_user, agent_serialization_manager, agent_with_files): """Test that file content is included in export""" - agent_id, source_id, file_id = agent_with_files + agent_id, _source_id, _file_id = agent_with_files exported = await agent_serialization_manager.export([agent_id], actor=default_user) diff --git a/tests/test_client.py b/tests/test_client.py index 9efed475..61c4faa5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -60,7 +60,7 @@ def mock_openai_server(): self.end_headers() self.wfile.write(body) - def do_GET(self): # noqa: N802 + def do_GET(self): # Support OpenAI model listing used during provider sync. if self.path in ("/v1/models", "/models"): self._send_json( @@ -78,7 +78,7 @@ def mock_openai_server(): self._send_json(404, {"error": {"message": f"Not found: {self.path}"}}) - def do_POST(self): # noqa: N802 + def do_POST(self): # Support embeddings endpoint if self.path not in ("/v1/embeddings", "/embeddings"): self._send_json(404, {"error": {"message": f"Not found: {self.path}"}}) @@ -739,7 +739,7 @@ def test_initial_sequence(client: Letta): # list messages messages = client.agents.messages.list(agent_id=agent.id).items - response = client.agents.messages.create( + client.agents.messages.create( agent_id=agent.id, messages=[ MessageCreateParam( @@ -803,7 +803,7 @@ def test_attach_sleeptime_block(client: Letta): group_id = agent.multi_agent_group.id group = client.groups.retrieve(group_id=group_id) agent_ids = group.agent_ids - sleeptime_id = [id for id in agent_ids if id != agent.id][0] + sleeptime_id = next(id for id in agent_ids if id != agent.id) # attach a new block block = client.blocks.create(label="test", value="test") # , project_id="test") @@ -891,7 +891,6 @@ def test_agent_generate_with_system_prompt(client: Letta, agent: AgentState): def test_agent_generate_with_model_override(client: Letta, agent: AgentState): """Test generate endpoint with model override.""" # Get the agent's current model - original_model = agent.llm_config.model # Use OpenAI model (more likely to be available in test environment) override_model_handle = "openai/gpt-4o-mini" diff --git a/tests/test_google_embeddings.py b/tests/test_google_embeddings.py index 5f879933..3304135b 100644 --- a/tests/test_google_embeddings.py +++ b/tests/test_google_embeddings.py @@ -2,14 +2,13 @@ import httpx import pytest from dotenv import load_dotenv -from letta.embeddings import GoogleEmbeddings # Adjust the import based on your module structure +from letta.embeddings import GoogleEmbeddings # type: ignore[import-untyped] # Adjust the import based on your module structure load_dotenv() import os import threading import time -import pytest from letta_client import CreateBlock, Letta as LettaSDKClient, MessageCreate SERVER_PORT = 8283 diff --git a/tests/test_internal_agents_count.py b/tests/test_internal_agents_count.py index 8223990f..34997a64 100644 --- a/tests/test_internal_agents_count.py +++ b/tests/test_internal_agents_count.py @@ -56,7 +56,7 @@ def test_agents(client: Letta) -> List[AgentState]: for agent in agents: try: client.agents.delete(agent.id) - except: + except Exception: pass diff --git a/tests/test_letta_agent_batch.py b/tests/test_letta_agent_batch.py index c2f5b718..9dc1cfb0 100644 --- a/tests/test_letta_agent_batch.py +++ b/tests/test_letta_agent_batch.py @@ -1,4 +1,5 @@ import asyncio +import itertools from datetime import datetime, timezone from typing import Tuple from unittest.mock import AsyncMock, patch @@ -769,7 +770,7 @@ def _assert_descending_order(messages): if len(messages) <= 1: return True - for prev, next in zip(messages[:-1], messages[1:]): + for prev, next in itertools.pairwise(messages): assert prev.created_at >= next.created_at, ( f"Order violation: {prev.id} ({prev.created_at}) followed by {next.id} ({next.created_at})" ) diff --git a/tests/test_llm_clients.py b/tests/test_llm_clients.py index 7ceb278c..54e97284 100644 --- a/tests/test_llm_clients.py +++ b/tests/test_llm_clients.py @@ -101,7 +101,7 @@ async def test_send_llm_batch_request_async_mismatched_keys(anthropic_client, mo a ValueError is raised. """ mismatched_tools = {"agent-2": []} # Different agent ID than in the messages mapping. - with pytest.raises(ValueError, match="Agent mappings for messages and tools must use the same agent_ids."): + with pytest.raises(ValueError, match=r"Agent mappings for messages and tools must use the same agent_ids."): await anthropic_client.send_llm_batch_request_async( AgentType.memgpt_agent, mock_agent_messages, mismatched_tools, mock_agent_llm_config ) diff --git a/tests/test_minimax_client.py b/tests/test_minimax_client.py index 32ac1d61..1e596fa3 100644 --- a/tests/test_minimax_client.py +++ b/tests/test_minimax_client.py @@ -4,10 +4,13 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from letta.llm_api.minimax_client import MINIMAX_BASE_URL, MiniMaxClient +from letta.llm_api.minimax_client import MiniMaxClient from letta.schemas.enums import AgentType from letta.schemas.llm_config import LLMConfig +# MiniMax API base URL +MINIMAX_BASE_URL = "https://api.minimax.io/anthropic" + class TestMiniMaxClient: """Tests for MiniMaxClient.""" @@ -55,7 +58,7 @@ class TestMiniMaxClient: # Mock BYOK to return no override self.client.get_byok_overrides = MagicMock(return_value=(None, None, None)) - client = self.client._get_anthropic_client(self.llm_config, async_client=False) + self.client._get_anthropic_client(self.llm_config, async_client=False) mock_anthropic.Anthropic.assert_called_once_with( api_key="test-api-key", @@ -73,7 +76,7 @@ class TestMiniMaxClient: # Mock BYOK to return no override self.client.get_byok_overrides = MagicMock(return_value=(None, None, None)) - client = self.client._get_anthropic_client(self.llm_config, async_client=True) + self.client._get_anthropic_client(self.llm_config, async_client=True) mock_anthropic.AsyncAnthropic.assert_called_once_with( api_key="test-api-key", @@ -100,7 +103,7 @@ class TestMiniMaxClientTemperatureClamping: """Verify build_request_data is called for temperature clamping.""" # This is a basic test to ensure the method exists and can be called mock_build.return_value = {"temperature": 0.7} - result = self.client.build_request_data( + self.client.build_request_data( agent_type=AgentType.letta_v1_agent, messages=[], llm_config=self.llm_config, @@ -214,7 +217,7 @@ class TestMiniMaxClientUsesNonBetaAPI: mock_anthropic_client.messages.create.return_value = mock_response mock_get_client.return_value = mock_anthropic_client - result = client.request({"model": "MiniMax-M2.1"}, llm_config) + client.request({"model": "MiniMax-M2.1"}, llm_config) # Verify messages.create was called (not beta.messages.create) mock_anthropic_client.messages.create.assert_called_once() @@ -239,7 +242,7 @@ class TestMiniMaxClientUsesNonBetaAPI: mock_anthropic_client.messages.create.return_value = mock_response mock_get_client.return_value = mock_anthropic_client - result = await client.request_async({"model": "MiniMax-M2.1"}, llm_config) + await client.request_async({"model": "MiniMax-M2.1"}, llm_config) # Verify messages.create was called (not beta.messages.create) mock_anthropic_client.messages.create.assert_called_once() @@ -261,7 +264,7 @@ class TestMiniMaxClientUsesNonBetaAPI: mock_anthropic_client.messages.create.return_value = mock_stream mock_get_client.return_value = mock_anthropic_client - result = await client.stream_async({"model": "MiniMax-M2.1"}, llm_config) + await client.stream_async({"model": "MiniMax-M2.1"}, llm_config) # Verify messages.create was called (not beta.messages.create) mock_anthropic_client.messages.create.assert_called_once() diff --git a/tests/test_prompt_caching.py b/tests/test_prompt_caching.py index a64f0b12..c7de0315 100644 --- a/tests/test_prompt_caching.py +++ b/tests/test_prompt_caching.py @@ -542,7 +542,7 @@ async def test_prompt_caching_cache_invalidation_on_memory_update( try: # Message 1: Establish cache - response1 = await async_client.agents.messages.create( + await async_client.agents.messages.create( agent_id=agent.id, messages=[MessageCreateParam(role="user", content="Hello!")], ) diff --git a/tests/test_redis_client.py b/tests/test_redis_client.py index 7017de9e..11bb9bc6 100644 --- a/tests/test_redis_client.py +++ b/tests/test_redis_client.py @@ -23,4 +23,4 @@ async def test_redis_client(): assert await redis_client.smismember(k, "invalid") == 0 assert await redis_client.smismember(k, v[0]) == 1 assert await redis_client.smismember(k, v[:2]) == [1, 1] - assert await redis_client.smismember(k, v[2:] + ["invalid"]) == [1, 0] + assert await redis_client.smismember(k, [*v[2:], "invalid"]) == [1, 0] diff --git a/tests/test_sdk_client.py b/tests/test_sdk_client.py index 9414d9c1..22e352cf 100644 --- a/tests/test_sdk_client.py +++ b/tests/test_sdk_client.py @@ -6,7 +6,7 @@ import textwrap import threading import time import uuid -from typing import List, Type +from typing import ClassVar, List, Type import pytest from dotenv import load_dotenv @@ -379,7 +379,7 @@ def test_add_and_manage_tags_for_agent(client: LettaSDKClient): assert len(agent.tags) == 0 # Step 1: Add multiple tags to the agent - updated_agent = client.agents.update(agent_id=agent.id, tags=tags_to_add) + client.agents.update(agent_id=agent.id, tags=tags_to_add) # Add small delay to ensure tags are persisted time.sleep(0.1) @@ -397,7 +397,7 @@ def test_add_and_manage_tags_for_agent(client: LettaSDKClient): # Step 4: Delete a specific tag from the agent and verify its removal tag_to_delete = tags_to_add.pop() - updated_agent = client.agents.update(agent_id=agent.id, tags=tags_to_add) + client.agents.update(agent_id=agent.id, tags=tags_to_add) # Verify the tag is removed from the agent's tags - explicitly request tags remaining_tags = client.agents.retrieve(agent_id=agent.id, include=["agent.tags"]).tags @@ -426,7 +426,7 @@ def test_reset_messages(client: LettaSDKClient): try: # Send a message - response = client.agents.messages.create( + client.agents.messages.create( agent_id=agent.id, messages=[MessageCreateParam(role="user", content="Hello")], ) @@ -542,7 +542,6 @@ def test_list_files_for_agent(client: LettaSDKClient): raise RuntimeError(f"File {file_metadata.id} not found") if file_metadata.processing_status == "error": raise RuntimeError(f"File processing failed: {getattr(file_metadata, 'error_message', 'Unknown error')}") - test_file = file_metadata agent = client.agents.create( memory_blocks=[CreateBlockParam(label="persona", value="test")], @@ -604,7 +603,7 @@ def test_modify_message(client: LettaSDKClient): try: # Send a message - response = client.agents.messages.create( + client.agents.messages.create( agent_id=agent.id, messages=[MessageCreateParam(role="user", content="Original message")], ) @@ -987,11 +986,6 @@ def test_function_always_error(client: LettaSDKClient, agent: AgentState): def test_agent_creation(client: LettaSDKClient): """Test that block IDs are properly attached when creating an agent.""" - sleeptime_agent_system = """ - You are a helpful agent. You will be provided with a list of memory blocks and a user preferences block. - You should use the memory blocks to remember information about the user and their preferences. - You should also use the user preferences block to remember information about the user's preferences. - """ # Create a test block that will represent user preferences user_preferences_block = client.blocks.create( @@ -1255,7 +1249,7 @@ def test_pydantic_inventory_management_tool(e2b_sandbox_mode, client: LettaSDKCl name: str = "manage_inventory" args_schema: Type[BaseModel] = InventoryEntryData description: str = "Update inventory catalogue with a new data entry" - tags: List[str] = ["inventory", "shop"] + tags: ClassVar[List[str]] = ["inventory", "shop"] def run(self, data: InventoryEntry, quantity_change: int) -> bool: print(f"Updated inventory for {data.item.name} with a quantity change of {quantity_change}") @@ -2381,7 +2375,7 @@ def test_create_agent_with_tools(client: LettaSDKClient) -> None: name: str = "manage_inventory" args_schema: Type[BaseModel] = InventoryEntryData description: str = "Update inventory catalogue with a new data entry" - tags: List[str] = ["inventory", "shop"] + tags: ClassVar[List[str]] = ["inventory", "shop"] def run(self, data: InventoryEntry, quantity_change: int) -> bool: """ diff --git a/tests/test_server.py b/tests/test_server.py index 67b3726c..48dfc51b 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -83,7 +83,7 @@ async def custom_anthropic_provider(server: SyncServer, user_id: str): @pytest.fixture async def agent(server: SyncServer, user: User): - actor = await server.user_manager.get_actor_or_default_async() + await server.user_manager.get_actor_or_default_async() agent = await server.create_agent_async( CreateAgent( agent_type="memgpt_v2_agent", @@ -129,7 +129,6 @@ async def test_messages_with_provider_override(server: SyncServer, custom_anthro run_id=run.id, ) usage = response.usage - messages = response.messages get_messages_response = await server.message_manager.list_messages(agent_id=agent.id, actor=actor, after=existing_messages[-1].id) @@ -228,7 +227,6 @@ async def test_messages_with_provider_override_legacy_agent(server: SyncServer, run_id=run.id, ) usage = response.usage - messages = response.messages get_messages_response = await server.message_manager.list_messages(agent_id=agent.id, actor=actor, after=existing_messages[-1].id) diff --git a/tests/test_server_providers.py b/tests/test_server_providers.py index 57dc256f..9c3cc7ff 100644 --- a/tests/test_server_providers.py +++ b/tests/test_server_providers.py @@ -110,7 +110,6 @@ async def test_sync_base_providers_handles_race_condition(default_user, provider # Mock a race condition: list returns empty, but create fails with UniqueConstraintViolation original_list = provider_manager.list_providers_async - original_create = provider_manager.create_provider_async call_count = {"count": 0} @@ -2030,14 +2029,14 @@ async def test_get_enabled_providers_async_queries_database(default_user, provid api_key="sk-test-key", base_url="https://api.openai.com/v1", ) - base_provider = await provider_manager.create_provider_async(base_provider_create, actor=default_user, is_byok=False) + await provider_manager.create_provider_async(base_provider_create, actor=default_user, is_byok=False) byok_provider_create = ProviderCreate( name=f"test-byok-provider-{test_id}", provider_type=ProviderType.anthropic, api_key="sk-test-byok-key", ) - byok_provider = await provider_manager.create_provider_async(byok_provider_create, actor=default_user, is_byok=True) + await provider_manager.create_provider_async(byok_provider_create, actor=default_user, is_byok=True) # Create server instance - importantly, don't set _enabled_providers # This ensures we're testing database queries, not in-memory list @@ -2182,7 +2181,7 @@ async def test_byok_provider_api_key_stored_in_db(default_user, provider_manager provider_type=ProviderType.openai, api_key="sk-byok-should-be-stored", ) - byok_provider = await provider_manager.create_provider_async(byok_provider_create, actor=default_user, is_byok=True) + await provider_manager.create_provider_async(byok_provider_create, actor=default_user, is_byok=True) # Retrieve the provider from database providers = await provider_manager.list_providers_async(name=f"test-byok-with-key-{test_id}", actor=default_user) @@ -2573,7 +2572,7 @@ async def test_byok_provider_last_synced_triggers_sync_when_null(default_user, p with patch.object(Provider, "cast_to_subtype", return_value=mock_typed_provider): # List BYOK models - should trigger sync because last_synced is null - byok_models = await server.list_llm_models_async( + await server.list_llm_models_async( actor=default_user, provider_category=[ProviderCategory.byok], ) diff --git a/tests/test_sonnet_nonnative_reasoning_buffering.py b/tests/test_sonnet_nonnative_reasoning_buffering.py index 7ca6890a..7373f0c6 100755 --- a/tests/test_sonnet_nonnative_reasoning_buffering.py +++ b/tests/test_sonnet_nonnative_reasoning_buffering.py @@ -84,7 +84,7 @@ def agent_factory(client: Letta): for agent_state in created_agents: try: client.agents.delete(agent_state.id) - except: + except Exception: pass # Agent might have already been deleted diff --git a/tests/test_sources.py b/tests/test_sources.py index 8cac5a46..8ceda0a5 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -89,9 +89,9 @@ def client() -> LettaSDKClient: @pytest.fixture def agent_state(disable_pinecone, client: LettaSDKClient): - open_file_tool = list(client.tools.list(name="open_files"))[0] - search_files_tool = list(client.tools.list(name="semantic_search_files"))[0] - grep_tool = list(client.tools.list(name="grep_files"))[0] + open_file_tool = next(iter(client.tools.list(name="open_files"))) + search_files_tool = next(iter(client.tools.list(name="semantic_search_files"))) + grep_tool = next(iter(client.tools.list(name="grep_files"))) agent_state = client.agents.create( name="test_sources_agent", @@ -745,13 +745,13 @@ def test_duplicate_file_renaming(disable_pinecone, disable_turbopuffer, client: file_path = "tests/data/test.txt" with open(file_path, "rb") as f: - first_file = client.folders.files.upload(folder_id=source.id, file=f) + client.folders.files.upload(folder_id=source.id, file=f) with open(file_path, "rb") as f: - second_file = client.folders.files.upload(folder_id=source.id, file=f) + client.folders.files.upload(folder_id=source.id, file=f) with open(file_path, "rb") as f: - third_file = client.folders.files.upload(folder_id=source.id, file=f) + client.folders.files.upload(folder_id=source.id, file=f) # Get all uploaded files files = list(client.folders.files.list(folder_id=source.id, limit=10)) @@ -821,7 +821,7 @@ def test_duplicate_file_handling_replace(disable_pinecone, disable_turbopuffer, f.write(replacement_content) # Upload replacement file with REPLACE duplicate handling - replacement_file = upload_file_and_wait(client, source.id, temp_file_path, duplicate_handling="replace") + upload_file_and_wait(client, source.id, temp_file_path, duplicate_handling="replace") # Verify we still have only 1 file (replacement, not addition) files_after_replace = list(client.folders.files.list(folder_id=source.id, limit=10)) diff --git a/tests/test_tool_rule_solver.py b/tests/test_tool_rule_solver.py index 96dda1eb..1facf57f 100644 --- a/tests/test_tool_rule_solver.py +++ b/tests/test_tool_rule_solver.py @@ -77,7 +77,7 @@ def test_get_allowed_tool_names_no_matching_rule_error(): solver = ToolRulesSolver(tool_rules=[init_rule]) solver.register_tool_call(UNRECOGNIZED_TOOL) - with pytest.raises(ValueError, match="No valid tools found based on tool rules."): + with pytest.raises(ValueError, match=r"No valid tools found based on tool rules."): solver.get_allowed_tool_names(set(), error_on_empty=True) @@ -119,7 +119,7 @@ def test_conditional_tool_rule(): def test_invalid_conditional_tool_rule(): - with pytest.raises(ValueError, match="Conditional tool rule must have at least one child tool."): + with pytest.raises(ValueError, match=r"Conditional tool rule must have at least one child tool."): ConditionalToolRule(tool_name=START_TOOL, default_child=END_TOOL, child_output_mapping={}) @@ -402,7 +402,7 @@ def test_cross_type_hash_distinguishes_types(a, b): ) def test_equality_with_non_rule_objects(rule): assert rule != object() - assert rule != None # noqa: E711 + assert rule != None def test_conditional_tool_rule_mapping_order_and_hash(): diff --git a/tests/test_tool_schema_parsing.py b/tests/test_tool_schema_parsing.py index 1ea9865f..9d960235 100644 --- a/tests/test_tool_schema_parsing.py +++ b/tests/test_tool_schema_parsing.py @@ -115,7 +115,7 @@ def test_derive_openai_json_schema(): # Collect results and check for failures for schema_name, result in results: try: - schema_name_result, success = result.get(timeout=60) # Wait for the result with timeout + _schema_name_result, success = result.get(timeout=60) # Wait for the result with timeout assert success, f"Test for {schema_name} failed" print(f"Test for {schema_name} passed") except Exception as e: diff --git a/tests/test_utils.py b/tests/test_utils.py index 3e23a0b8..2aea57e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -492,7 +492,7 @@ def test_line_chunker_out_of_range_start(): chunker = LineChunker() # Test with start beyond file length - should raise ValueError - with pytest.raises(ValueError, match="File test.py has only 3 lines, but requested offset 6 is out of range"): + with pytest.raises(ValueError, match=r"File test.py has only 3 lines, but requested offset 6 is out of range"): chunker.chunk_text(file, start=5, end=6, validate_range=True) @@ -530,7 +530,7 @@ def test_line_chunker_edge_case_single_line(): assert "1: only line" in result[1] # Test out of range for single line file - should raise error - with pytest.raises(ValueError, match="File single.py has only 1 lines, but requested offset 2 is out of range"): + with pytest.raises(ValueError, match=r"File single.py has only 1 lines, but requested offset 2 is out of range"): chunker.chunk_text(file, start=1, end=2, validate_range=True) @@ -540,7 +540,7 @@ def test_line_chunker_validation_disabled_allows_out_of_range(): chunker = LineChunker() # Test 1: Out of bounds start should always raise error, even with validation disabled - with pytest.raises(ValueError, match="File test.py has only 3 lines, but requested offset 6 is out of range"): + with pytest.raises(ValueError, match=r"File test.py has only 3 lines, but requested offset 6 is out of range"): chunker.chunk_text(file, start=5, end=10, validate_range=False) # Test 2: With validation disabled, start >= end should be allowed (but gives empty result) @@ -561,7 +561,7 @@ def test_line_chunker_only_start_parameter(): assert "3: line3" in result[2] # Test start at end of file - should raise error - with pytest.raises(ValueError, match="File test.py has only 3 lines, but requested offset 4 is out of range"): + with pytest.raises(ValueError, match=r"File test.py has only 3 lines, but requested offset 4 is out of range"): chunker.chunk_text(file, start=3, validate_range=True) @@ -653,10 +653,10 @@ def test_validate_function_response_strict_mode_none(): def test_validate_function_response_strict_mode_violation(): """Test strict mode raises ValueError for non-string/None types""" - with pytest.raises(ValueError, match="Strict mode violation. Function returned type: int"): + with pytest.raises(ValueError, match=r"Strict mode violation. Function returned type: int"): validate_function_response(42, return_char_limit=100, strict=True) - with pytest.raises(ValueError, match="Strict mode violation. Function returned type: dict"): + with pytest.raises(ValueError, match=r"Strict mode violation. Function returned type: dict"): validate_function_response({"key": "value"}, return_char_limit=100, strict=True) diff --git a/uv.lock b/uv.lock index 73a762d1..93fb6e8c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <3.14" resolution-markers = [ "python_full_version >= '3.13'", @@ -2693,6 +2693,7 @@ dependencies = [ { name = "temporalio" }, { name = "tqdm" }, { name = "trafilatura" }, + { name = "ty" }, { name = "typer" }, ] @@ -2874,7 +2875,7 @@ requires-dist = [ { name = "readability-lxml" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=6.2.0" }, { name = "rich", specifier = ">=13.9.4" }, - { name = "ruff", extras = ["dev"], specifier = ">=0.12.10" }, + { name = "ruff", specifier = ">=0.12.10" }, { name = "sentry-sdk", extras = ["fastapi"], specifier = "==2.19.1" }, { name = "setuptools", specifier = ">=70" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.41" }, @@ -2890,6 +2891,7 @@ requires-dist = [ { name = "tqdm", specifier = ">=4.66.1" }, { name = "trafilatura" }, { name = "turbopuffer", marker = "extra == 'external-tools'", specifier = ">=0.5.17" }, + { name = "ty", specifier = ">=0.0.17" }, { name = "typer", specifier = ">=0.15.2" }, { name = "uvicorn", marker = "extra == 'desktop'", specifier = "==0.29.0" }, { name = "uvicorn", marker = "extra == 'server'", specifier = "==0.29.0" }, @@ -5489,28 +5491,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.10" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, - { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, - { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, - { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, - { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, - { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, - { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, - { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -6012,6 +6013,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/26/61f236b52fd5e9161e21e1074c8133c9402945f4cf13612d9f9792ea0b0f/turbopuffer-0.6.5-py3-none-any.whl", hash = "sha256:d0c2261fcce5fa0ae9d82b103c3cf5d90cb2da263b76a41d8f121714f60a4e5c", size = 104879, upload-time = "2025-08-18T20:58:14.171Z" }, ] +[[package]] +name = "ty" +version = "0.0.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, + { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, + { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, +] + [[package]] name = "typeguard" version = "4.4.4"