letta-nightly 0.8.0.dev20250606195656__py3-none-any.whl → 0.8.2.dev20250606215616__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +1 -1
  3. letta/agents/letta_agent.py +49 -29
  4. letta/agents/letta_agent_batch.py +1 -2
  5. letta/agents/voice_agent.py +19 -13
  6. letta/agents/voice_sleeptime_agent.py +11 -3
  7. letta/constants.py +18 -0
  8. letta/data_sources/__init__.py +0 -0
  9. letta/data_sources/redis_client.py +282 -0
  10. letta/errors.py +0 -4
  11. letta/functions/function_sets/files.py +58 -0
  12. letta/functions/schema_generator.py +18 -1
  13. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  14. letta/helpers/datetime_helpers.py +47 -3
  15. letta/helpers/decorators.py +69 -0
  16. letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
  17. letta/interfaces/anthropic_streaming_interface.py +43 -24
  18. letta/interfaces/openai_streaming_interface.py +21 -19
  19. letta/llm_api/anthropic.py +1 -1
  20. letta/llm_api/anthropic_client.py +22 -14
  21. letta/llm_api/google_vertex_client.py +1 -1
  22. letta/llm_api/helpers.py +36 -30
  23. letta/llm_api/llm_api_tools.py +1 -1
  24. letta/llm_api/llm_client_base.py +29 -1
  25. letta/llm_api/openai.py +1 -1
  26. letta/llm_api/openai_client.py +6 -8
  27. letta/local_llm/chat_completion_proxy.py +1 -1
  28. letta/memory.py +1 -1
  29. letta/orm/enums.py +1 -0
  30. letta/orm/file.py +80 -3
  31. letta/orm/files_agents.py +13 -0
  32. letta/orm/sqlalchemy_base.py +34 -11
  33. letta/otel/__init__.py +0 -0
  34. letta/otel/context.py +25 -0
  35. letta/otel/events.py +0 -0
  36. letta/otel/metric_registry.py +122 -0
  37. letta/otel/metrics.py +66 -0
  38. letta/otel/resource.py +26 -0
  39. letta/{tracing.py → otel/tracing.py} +55 -78
  40. letta/plugins/README.md +22 -0
  41. letta/plugins/__init__.py +0 -0
  42. letta/plugins/defaults.py +11 -0
  43. letta/plugins/plugins.py +72 -0
  44. letta/schemas/enums.py +8 -0
  45. letta/schemas/file.py +12 -0
  46. letta/schemas/tool.py +4 -0
  47. letta/server/db.py +7 -7
  48. letta/server/rest_api/app.py +8 -6
  49. letta/server/rest_api/routers/v1/agents.py +37 -36
  50. letta/server/rest_api/routers/v1/groups.py +3 -3
  51. letta/server/rest_api/routers/v1/sources.py +26 -3
  52. letta/server/rest_api/utils.py +9 -6
  53. letta/server/server.py +18 -12
  54. letta/services/agent_manager.py +185 -193
  55. letta/services/block_manager.py +1 -1
  56. letta/services/context_window_calculator/token_counter.py +3 -2
  57. letta/services/file_processor/chunker/line_chunker.py +34 -0
  58. letta/services/file_processor/file_processor.py +40 -11
  59. letta/services/file_processor/parser/mistral_parser.py +11 -1
  60. letta/services/files_agents_manager.py +96 -7
  61. letta/services/group_manager.py +6 -6
  62. letta/services/helpers/agent_manager_helper.py +373 -3
  63. letta/services/identity_manager.py +1 -1
  64. letta/services/job_manager.py +1 -1
  65. letta/services/llm_batch_manager.py +1 -1
  66. letta/services/message_manager.py +1 -1
  67. letta/services/organization_manager.py +1 -1
  68. letta/services/passage_manager.py +1 -1
  69. letta/services/per_agent_lock_manager.py +1 -1
  70. letta/services/provider_manager.py +1 -1
  71. letta/services/sandbox_config_manager.py +1 -1
  72. letta/services/source_manager.py +178 -19
  73. letta/services/step_manager.py +2 -2
  74. letta/services/summarizer/summarizer.py +1 -1
  75. letta/services/telemetry_manager.py +1 -1
  76. letta/services/tool_executor/builtin_tool_executor.py +117 -0
  77. letta/services/tool_executor/composio_tool_executor.py +53 -0
  78. letta/services/tool_executor/core_tool_executor.py +474 -0
  79. letta/services/tool_executor/files_tool_executor.py +131 -0
  80. letta/services/tool_executor/mcp_tool_executor.py +45 -0
  81. letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
  82. letta/services/tool_executor/tool_execution_manager.py +34 -14
  83. letta/services/tool_executor/tool_execution_sandbox.py +1 -1
  84. letta/services/tool_executor/tool_executor.py +3 -802
  85. letta/services/tool_executor/tool_executor_base.py +43 -0
  86. letta/services/tool_manager.py +55 -59
  87. letta/services/tool_sandbox/e2b_sandbox.py +1 -1
  88. letta/services/tool_sandbox/local_sandbox.py +6 -3
  89. letta/services/user_manager.py +6 -3
  90. letta/settings.py +21 -1
  91. letta/utils.py +7 -2
  92. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/METADATA +4 -2
  93. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/RECORD +96 -74
  94. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/LICENSE +0 -0
  95. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/WHEEL +0 -0
  96. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  import inspect
2
2
  import re
3
- import sys
4
3
  import time
5
4
  from functools import wraps
6
5
  from typing import Any, Dict, List, Optional
@@ -11,15 +10,18 @@ from fastapi.responses import JSONResponse
11
10
  from opentelemetry import trace
12
11
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
13
12
  from opentelemetry.instrumentation.requests import RequestsInstrumentor
14
- from opentelemetry.sdk.resources import Resource
15
13
  from opentelemetry.sdk.trace import TracerProvider
16
14
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
17
15
  from opentelemetry.trace import Status, StatusCode
18
16
 
19
- from letta import __version__ as letta_version
17
+ from letta.log import get_logger
18
+ from letta.otel.resource import get_resource, is_pytest_environment
19
+ from letta.settings import settings
20
20
 
21
+ logger = get_logger(__name__) # TODO: set up logger config for this
21
22
  tracer = trace.get_tracer(__name__)
22
23
  _is_tracing_initialized = False
24
+
23
25
  _excluded_v1_endpoints_regex: List[str] = [
24
26
  # "^GET /v1/agents/(?P<agent_id>[^/]+)/messages$",
25
27
  # "^GET /v1/agents/(?P<agent_id>[^/]+)/context$",
@@ -30,11 +32,7 @@ _excluded_v1_endpoints_regex: List[str] = [
30
32
  ]
31
33
 
32
34
 
33
- def is_pytest_environment():
34
- return "pytest" in sys.modules
35
-
36
-
37
- async def trace_request_middleware(request: Request, call_next):
35
+ async def _trace_request_middleware(request: Request, call_next):
38
36
  if not _is_tracing_initialized:
39
37
  return await call_next(request)
40
38
  initial_span_name = f"{request.method} {request.url.path}"
@@ -56,7 +54,7 @@ async def trace_request_middleware(request: Request, call_next):
56
54
  raise
57
55
 
58
56
 
59
- async def update_trace_attributes(request: Request):
57
+ async def _update_trace_attributes(request: Request):
60
58
  """Dependency to update trace attributes after FastAPI has processed the request"""
61
59
  if not _is_tracing_initialized:
62
60
  return
@@ -78,35 +76,19 @@ async def update_trace_attributes(request: Request):
78
76
  for key, value in request.path_params.items():
79
77
  span.set_attribute(f"http.{key}", value)
80
78
 
81
- # Add user ID if available
82
- user_id = request.headers.get("user_id")
83
- if user_id:
84
- span.set_attribute("user.id", user_id)
85
-
86
- # Add organization_id if available
87
- organization_id = request.headers.get("x-organization-id")
88
- if organization_id:
89
- span.set_attribute("organization.id", organization_id)
90
-
91
- # Add project_id if available
92
- project_id = request.headers.get("x-project-id")
93
- if project_id:
94
- span.set_attribute("project.id", project_id)
95
-
96
- # Add agent_id if available
97
- agent_id = request.headers.get("x-agent-id")
98
- if agent_id:
99
- span.set_attribute("agent.id", agent_id)
100
-
101
- # Add template_id if available
102
- template_id = request.headers.get("x-template-id")
103
- if template_id:
104
- span.set_attribute("template.id", template_id)
105
-
106
- # Add base_template_id if available
107
- base_template_id = request.headers.get("x-base-template-id")
108
- if base_template_id:
109
- span.set_attribute("base_template.id", base_template_id)
79
+ # Add the following headers to span if available
80
+ header_attributes = {
81
+ "user_id": "user.id",
82
+ "x-organization-id": "organization.id",
83
+ "x-project-id": "project.id",
84
+ "x-agent-id": "agent.id",
85
+ "x-template-id": "template.id",
86
+ "x-base-template-id": "base_template.id",
87
+ }
88
+ for header_key, span_key in header_attributes.items():
89
+ header_value = request.headers.get(header_key)
90
+ if header_value:
91
+ span.set_attribute(span_key, header_value)
110
92
 
111
93
  # Add request body if available
112
94
  try:
@@ -117,7 +99,7 @@ async def update_trace_attributes(request: Request):
117
99
  pass
118
100
 
119
101
 
120
- async def trace_error_handler(_request: Request, exc: Exception) -> JSONResponse:
102
+ async def _trace_error_handler(_request: Request, exc: Exception) -> JSONResponse:
121
103
  status_code = getattr(exc, "status_code", 500)
122
104
  error_msg = str(exc)
123
105
 
@@ -142,49 +124,44 @@ def setup_tracing(
142
124
  ) -> None:
143
125
  if is_pytest_environment():
144
126
  return
127
+ assert endpoint
145
128
 
146
129
  global _is_tracing_initialized
147
130
 
148
- provider = TracerProvider(resource=Resource.create({"service.name": service_name}))
149
- import uuid
131
+ tracer_provider = TracerProvider(resource=get_resource(service_name))
132
+ tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
133
+ _is_tracing_initialized = True
134
+ trace.set_tracer_provider(tracer_provider)
150
135
 
151
- provider = TracerProvider(
152
- resource=Resource.create(
153
- {
154
- "service.name": service_name,
155
- "device.id": uuid.getnode(), # MAC address as unique device identifier,
156
- "letta.version": letta_version,
157
- }
158
- )
159
- )
160
- if endpoint:
161
- provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
162
- _is_tracing_initialized = True
163
- trace.set_tracer_provider(provider)
164
-
165
- def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
166
- if hasattr(response, "status_code"):
167
- span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
168
-
169
- RequestsInstrumentor().instrument(response_hook=requests_callback)
170
-
171
- if app:
172
- # Add middleware first
173
- app.middleware("http")(trace_request_middleware)
174
-
175
- # Add dependency to v1 routes
176
- from letta.server.rest_api.routers.v1 import ROUTERS as v1_routes
177
-
178
- for router in v1_routes:
179
- for route in router.routes:
180
- full_path = ((next(iter(route.methods)) + " ") if route.methods else "") + "/v1" + route.path
181
- if not any(re.match(regex, full_path) for regex in _excluded_v1_endpoints_regex):
182
- route.dependencies.append(Depends(update_trace_attributes))
183
-
184
- # Register exception handlers
185
- app.exception_handler(HTTPException)(trace_error_handler)
186
- app.exception_handler(RequestValidationError)(trace_error_handler)
187
- app.exception_handler(Exception)(trace_error_handler)
136
+ # Instrumentors (e.g., RequestsInstrumentor)
137
+ def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
138
+ if hasattr(response, "status_code"):
139
+ span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
140
+
141
+ RequestsInstrumentor().instrument(response_hook=requests_callback)
142
+
143
+ if settings.sqlalchemy_tracing:
144
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
145
+
146
+ SQLAlchemyInstrumentor().instrument()
147
+
148
+ if app:
149
+ # Add middleware first
150
+ app.middleware("http")(_trace_request_middleware)
151
+
152
+ # Add dependency to v1 routes
153
+ from letta.server.rest_api.routers.v1 import ROUTERS as V1_ROUTES
154
+
155
+ for router in V1_ROUTES:
156
+ for route in router.routes:
157
+ full_path = ((next(iter(route.methods)) + " ") if route.methods else "") + "/v1" + route.path
158
+ if not any(re.match(regex, full_path) for regex in _excluded_v1_endpoints_regex):
159
+ route.dependencies.append(Depends(_update_trace_attributes))
160
+
161
+ # Register exception handlers for tracing
162
+ app.exception_handler(HTTPException)(_trace_error_handler)
163
+ app.exception_handler(RequestValidationError)(_trace_error_handler)
164
+ app.exception_handler(Exception)(_trace_error_handler)
188
165
 
189
166
 
190
167
  def trace_method(func):
@@ -0,0 +1,22 @@
1
+ ### Plugins
2
+
3
+ Plugins enable plug and play for various components.
4
+
5
+ Plugin configurations can be set in `letta.settings.settings`.
6
+
7
+ The plugins will take a delimited list of consisting of individual plugin configs:
8
+
9
+ `<plugin_name>.<config_name>=<class_or_function>`
10
+
11
+ joined by `;`
12
+
13
+ In the default configuration, the top level keys have values `plugin_name`,
14
+ the `config_name` is nested under and the `class_or_function` is defined
15
+ after in format `<module_path>:<name>`.
16
+
17
+ ```
18
+ DEFAULT_PLUGINS = {
19
+ "experimental_check": {
20
+ "default": "letta.plugins.defaults:is_experimental_enabled",
21
+ ...
22
+ ```
File without changes
@@ -0,0 +1,11 @@
1
+ from letta.settings import settings
2
+
3
+
4
+ def is_experimental_enabled(feature_name: str, **kwargs) -> bool:
5
+ if feature_name in ("async_agent_loop", "summarize"):
6
+ if not (kwargs.get("eligibility", False) and settings.use_experimental):
7
+ return False
8
+ return True
9
+
10
+ # Err on safety here, disabling experimental if not handled here.
11
+ return False
@@ -0,0 +1,72 @@
1
+ import importlib
2
+ from typing import Protocol, runtime_checkable
3
+
4
+ from letta.settings import settings
5
+
6
+
7
+ @runtime_checkable
8
+ class SummarizerProtocol(Protocol):
9
+ """What a summarizer must implement"""
10
+
11
+ async def summarize(self, text: str) -> str: ...
12
+ def get_name(self) -> str: ...
13
+
14
+
15
+ # Currently this supports one of each plugin type. This can be expanded in the future.
16
+ DEFAULT_PLUGINS = {
17
+ "experimental_check": {
18
+ "protocol": None,
19
+ "target": "letta.plugins.defaults:is_experimental_enabled",
20
+ },
21
+ "summarizer": {
22
+ "protocol": SummarizerProtocol,
23
+ "target": "letta.services.summarizer.summarizer:Summarizer",
24
+ },
25
+ }
26
+
27
+
28
+ def get_plugin(plugin_type: str):
29
+ """Get a plugin instance"""
30
+ plugin_register = dict(DEFAULT_PLUGINS, **settings.plugin_register_dict)
31
+ if plugin_type in plugin_register:
32
+ impl_path = plugin_register[plugin_type]["target"]
33
+ module_path, name = impl_path.split(":")
34
+ module = importlib.import_module(module_path)
35
+ plugin = getattr(module, name)
36
+ if type(plugin).__name__ == "function":
37
+ return plugin
38
+ elif type(plugin).__name__ == "class":
39
+ if plugin_register["protocol"] and not isinstance(plugin, type(plugin_register["protocol"])):
40
+ raise TypeError(f'{plugin} does not implement {type(plugin_register["protocol"]).__name__}')
41
+ return plugin()
42
+ raise TypeError("Unknown plugin type")
43
+
44
+
45
+ _experimental_checker = None
46
+ _summarizer = None
47
+
48
+
49
+ # TODO handle coroutines
50
+ # Convenience functions
51
+ def get_experimental_checker():
52
+ global _experimental_checker
53
+ if _experimental_checker is None:
54
+ _experimental_checker = get_plugin("experimental_check")
55
+ return _experimental_checker
56
+
57
+
58
+ def get_summarizer():
59
+ global _summarizer
60
+ if _summarizer is None:
61
+ _summarizer = get_plugin("summarizer")
62
+ return _summarizer
63
+
64
+
65
+ def reset_experimental_checker():
66
+ global _experimental_checker
67
+ _experimental_checker = None
68
+
69
+
70
+ def reset_summarizer():
71
+ global _summarizer
72
+ _summarizer = None
letta/schemas/enums.py CHANGED
@@ -87,3 +87,11 @@ class ToolRuleType(str, Enum):
87
87
  constrain_child_tools = "constrain_child_tools"
88
88
  max_count_per_step = "max_count_per_step"
89
89
  parent_last_tool = "parent_last_tool"
90
+
91
+
92
+ class FileProcessingStatus(str, Enum):
93
+ PENDING = "pending"
94
+ PARSING = "parsing"
95
+ EMBEDDING = "embedding"
96
+ COMPLETED = "completed"
97
+ ERROR = "error"
letta/schemas/file.py CHANGED
@@ -4,6 +4,7 @@ from typing import Optional
4
4
 
5
5
  from pydantic import Field
6
6
 
7
+ from letta.schemas.enums import FileProcessingStatus
7
8
  from letta.schemas.letta_base import LettaBase
8
9
 
9
10
 
@@ -34,12 +35,22 @@ class FileMetadata(FileMetadataBase):
34
35
  file_size: Optional[int] = Field(None, description="The size of the file in bytes.")
35
36
  file_creation_date: Optional[str] = Field(None, description="The creation date of the file.")
36
37
  file_last_modified_date: Optional[str] = Field(None, description="The last modified date of the file.")
38
+ processing_status: FileProcessingStatus = Field(
39
+ default=FileProcessingStatus.PENDING,
40
+ description="The current processing status of the file (e.g. pending, parsing, embedding, completed, error).",
41
+ )
42
+ error_message: Optional[str] = Field(default=None, description="Optional error message if the file failed processing.")
37
43
 
38
44
  # orm metadata, optional fields
39
45
  created_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The creation date of the file.")
40
46
  updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The update date of the file.")
41
47
  is_deleted: bool = Field(False, description="Whether this file is deleted or not.")
42
48
 
49
+ # This is optional, and only occasionally pulled in since it can be very large
50
+ content: Optional[str] = Field(
51
+ default=None, description="Optional full-text content of the file; only populated on demand due to its size."
52
+ )
53
+
43
54
 
44
55
  class FileAgentBase(LettaBase):
45
56
  """Base class for the FileMetadata-⇄-Agent association schemas"""
@@ -67,6 +78,7 @@ class FileAgent(FileAgentBase):
67
78
  )
68
79
  agent_id: str = Field(..., description="Unique identifier of the agent.")
69
80
  file_id: str = Field(..., description="Unique identifier of the file.")
81
+ file_name: str = Field(..., description="Name of the file.")
70
82
  is_open: bool = Field(True, description="True if the agent currently has the file open.")
71
83
  visible_content: Optional[str] = Field(
72
84
  None,
letta/schemas/tool.py CHANGED
@@ -7,6 +7,7 @@ from letta.constants import (
7
7
  FUNCTION_RETURN_CHAR_LIMIT,
8
8
  LETTA_BUILTIN_TOOL_MODULE_NAME,
9
9
  LETTA_CORE_TOOL_MODULE_NAME,
10
+ LETTA_FILES_TOOL_MODULE_NAME,
10
11
  LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
11
12
  LETTA_VOICE_TOOL_MODULE_NAME,
12
13
  MCP_TOOL_TAG_NAME_PREFIX,
@@ -106,6 +107,9 @@ class Tool(BaseTool):
106
107
  elif self.tool_type in {ToolType.LETTA_BUILTIN}:
107
108
  # If it's letta voice tool, we generate the json_schema on the fly here
108
109
  self.json_schema = get_json_schema_from_module(module_name=LETTA_BUILTIN_TOOL_MODULE_NAME, function_name=self.name)
110
+ elif self.tool_type in {ToolType.LETTA_FILES_CORE}:
111
+ # If it's letta files tool, we generate the json_schema on the fly here
112
+ self.json_schema = get_json_schema_from_module(module_name=LETTA_FILES_TOOL_MODULE_NAME, function_name=self.name)
109
113
  elif self.tool_type in {ToolType.EXTERNAL_COMPOSIO}:
110
114
  # Composio schemas handled separately
111
115
  pass
letta/server/db.py CHANGED
@@ -13,8 +13,8 @@ from sqlalchemy.orm import sessionmaker
13
13
 
14
14
  from letta.config import LettaConfig
15
15
  from letta.log import get_logger
16
+ from letta.otel.tracing import trace_method
16
17
  from letta.settings import settings
17
- from letta.tracing import trace_method
18
18
 
19
19
  logger = get_logger(__name__)
20
20
 
@@ -131,7 +131,12 @@ class DatabaseRegistry:
131
131
  # Create async session factory
132
132
  self._async_engines["default"] = async_engine
133
133
  self._async_session_factories["default"] = async_sessionmaker(
134
- close_resets_only=False, autocommit=False, autoflush=False, bind=self._async_engines["default"], class_=AsyncSession
134
+ expire_on_commit=True,
135
+ close_resets_only=False,
136
+ autocommit=False,
137
+ autoflush=False,
138
+ bind=self._async_engines["default"],
139
+ class_=AsyncSession,
135
140
  )
136
141
  self._initialized["async"] = True
137
142
 
@@ -207,11 +212,6 @@ class DatabaseRegistry:
207
212
  self.initialize_sync()
208
213
  return self._engines.get(name)
209
214
 
210
- def get_async_engine(self, name: str = "default") -> AsyncEngine:
211
- """Get an async database engine by name."""
212
- self.initialize_async()
213
- return self._async_engines.get(name)
214
-
215
215
  def get_session_factory(self, name: str = "default") -> sessionmaker:
216
216
  """Get a session factory by name."""
217
217
  self.initialize_sync()
@@ -256,13 +256,15 @@ def create_application() -> "FastAPI":
256
256
  print(f"▶ Using OTLP tracing with endpoint: {otlp_endpoint}")
257
257
  env_name_suffix = os.getenv("ENV_NAME")
258
258
  service_name = f"letta-server-{env_name_suffix.lower()}" if env_name_suffix else "letta-server"
259
- from letta.tracing import setup_tracing
259
+ from letta.otel.metrics import setup_metrics
260
+ from letta.otel.tracing import setup_tracing
260
261
 
261
262
  setup_tracing(
262
263
  endpoint=otlp_endpoint,
263
264
  app=app,
264
265
  service_name=service_name,
265
266
  )
267
+ setup_metrics(endpoint=otlp_endpoint, app=app, service_name=service_name)
266
268
 
267
269
  for route in v1_routes:
268
270
  app.include_router(route, prefix=API_PREFIX)
@@ -331,7 +333,7 @@ def start_server(
331
333
  if (os.getenv("LOCAL_HTTPS") == "true") or "--localhttps" in sys.argv:
332
334
  print(f"▶ Server running at: https://{host or 'localhost'}:{port or REST_DEFAULT_PORT}")
333
335
  print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
334
- if importlib.util.find_spec("granian") is not None and settings.use_uvloop:
336
+ if importlib.util.find_spec("granian") is not None and settings.use_granian:
335
337
  from granian import Granian
336
338
 
337
339
  # Experimental Granian engine
@@ -339,14 +341,14 @@ def start_server(
339
341
  target="letta.server.rest_api.app:app",
340
342
  # factory=True,
341
343
  interface="asgi",
342
- address=host or "localhost",
344
+ address=host or "127.0.0.1", # Note granian address must be an ip address
343
345
  port=port or REST_DEFAULT_PORT,
344
346
  workers=settings.uvicorn_workers,
345
347
  # threads=
346
348
  reload=reload or settings.uvicorn_reload,
347
349
  reload_ignore_patterns=["openapi_letta.json"],
348
350
  reload_ignore_worker_failure=True,
349
- reload_tick=100,
351
+ reload_tick=4000, # set to 4s to prevent crashing on weird state
350
352
  # log_level="info"
351
353
  ssl_keyfile="certs/localhost-key.pem",
352
354
  ssl_cert="certs/localhost.pem",
@@ -380,14 +382,14 @@ def start_server(
380
382
  target="letta.server.rest_api.app:app",
381
383
  # factory=True,
382
384
  interface="asgi",
383
- address=host or "localhost",
385
+ address=host or "127.0.0.1", # Note granian address must be an ip address
384
386
  port=port or REST_DEFAULT_PORT,
385
387
  workers=settings.uvicorn_workers,
386
388
  # threads=
387
389
  reload=reload or settings.uvicorn_reload,
388
390
  reload_ignore_patterns=["openapi_letta.json"],
389
391
  reload_ignore_worker_failure=True,
390
- reload_tick=100,
392
+ reload_tick=4000, # set to 4s to prevent crashing on weird state
391
393
  # log_level="info"
392
394
  ).serve()
393
395
  else: