letta-nightly 0.5.1.dev20241106104104__py3-none-any.whl → 0.5.2.dev20241107104040__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.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (33) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +13 -7
  3. letta/agent_store/db.py +3 -1
  4. letta/cli/cli.py +14 -1
  5. letta/client/client.py +16 -8
  6. letta/constants.py +3 -0
  7. letta/functions/functions.py +1 -1
  8. letta/helpers/tool_rule_solver.py +1 -2
  9. letta/log.py +1 -1
  10. letta/main.py +3 -0
  11. letta/metadata.py +2 -0
  12. letta/orm/agents_tags.py +28 -0
  13. letta/orm/organization.py +1 -0
  14. letta/orm/sqlalchemy_base.py +16 -0
  15. letta/schemas/agent.py +5 -0
  16. letta/schemas/agents_tags.py +33 -0
  17. letta/schemas/block.py +3 -3
  18. letta/schemas/letta_response.py +110 -0
  19. letta/schemas/llm_config.py +7 -1
  20. letta/schemas/tool.py +6 -2
  21. letta/schemas/tool_rule.py +12 -2
  22. letta/server/rest_api/app.py +4 -1
  23. letta/server/rest_api/routers/v1/agents.py +2 -122
  24. letta/server/rest_api/routers/v1/tools.py +1 -1
  25. letta/server/rest_api/routers/v1/users.py +13 -1
  26. letta/server/server.py +73 -42
  27. letta/services/agents_tags_manager.py +64 -0
  28. letta/services/tool_manager.py +7 -16
  29. {letta_nightly-0.5.1.dev20241106104104.dist-info → letta_nightly-0.5.2.dev20241107104040.dist-info}/METADATA +4 -1
  30. {letta_nightly-0.5.1.dev20241106104104.dist-info → letta_nightly-0.5.2.dev20241107104040.dist-info}/RECORD +33 -30
  31. {letta_nightly-0.5.1.dev20241106104104.dist-info → letta_nightly-0.5.2.dev20241107104040.dist-info}/LICENSE +0 -0
  32. {letta_nightly-0.5.1.dev20241106104104.dist-info → letta_nightly-0.5.2.dev20241107104040.dist-info}/WHEEL +0 -0
  33. {letta_nightly-0.5.1.dev20241106104104.dist-info → letta_nightly-0.5.2.dev20241107104040.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.5.1"
1
+ __version__ = "0.5.2"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
letta/agent.py CHANGED
@@ -3,7 +3,6 @@ import inspect
3
3
  import traceback
4
4
  import warnings
5
5
  from abc import ABC, abstractmethod
6
- from lib2to3.fixer_util import is_list
7
6
  from typing import List, Literal, Optional, Tuple, Union
8
7
 
9
8
  from tqdm import tqdm
@@ -249,12 +248,21 @@ class Agent(BaseAgent):
249
248
  # initialize a tool rules solver
250
249
  if agent_state.tool_rules:
251
250
  # if there are tool rules, print out a warning
252
- warnings.warn("Tool rules only work reliably for the latest OpenAI models that support structured outputs.")
251
+ for rule in agent_state.tool_rules:
252
+ if not isinstance(rule, TerminalToolRule):
253
+ warnings.warn("Tool rules only work reliably for the latest OpenAI models that support structured outputs.")
254
+ break
253
255
  # add default rule for having send_message be a terminal tool
254
-
255
- if not is_list(agent_state.tool_rules):
256
+ if agent_state.tool_rules is None:
256
257
  agent_state.tool_rules = []
257
- agent_state.tool_rules.append(TerminalToolRule(tool_name="send_message"))
258
+ # Define the rule to add
259
+ send_message_terminal_rule = TerminalToolRule(tool_name="send_message")
260
+ # Check if an equivalent rule is already present
261
+ if not any(
262
+ isinstance(rule, TerminalToolRule) and rule.tool_name == send_message_terminal_rule.tool_name for rule in agent_state.tool_rules
263
+ ):
264
+ agent_state.tool_rules.append(send_message_terminal_rule)
265
+
258
266
  self.tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules)
259
267
 
260
268
  # gpt-4, gpt-3.5-turbo, ...
@@ -396,7 +404,6 @@ class Agent(BaseAgent):
396
404
  exec(tool.module, env)
397
405
  else:
398
406
  exec(tool.source_code, env)
399
-
400
407
  self.functions_python[tool.json_schema["name"]] = env[tool.json_schema["name"]]
401
408
  self.functions.append(tool.json_schema)
402
409
  except Exception as e:
@@ -788,7 +795,6 @@ class Agent(BaseAgent):
788
795
 
789
796
  # Update ToolRulesSolver state with last called function
790
797
  self.tool_rules_solver.update_tool_usage(function_name)
791
-
792
798
  # Update heartbeat request according to provided tool rules
793
799
  if self.tool_rules_solver.has_children_tools(function_name):
794
800
  heartbeat_request = True
letta/agent_store/db.py CHANGED
@@ -578,4 +578,6 @@ class SQLLiteStorageConnector(SQLStorageConnector):
578
578
  def attach_base():
579
579
  # This should be invoked in server.py to make sure Base gets initialized properly
580
580
  # DO NOT REMOVE
581
- print("Initializing database...")
581
+ from letta.utils import printd
582
+
583
+ printd("Initializing database...")
letta/cli/cli.py CHANGED
@@ -10,7 +10,7 @@ import letta.utils as utils
10
10
  from letta import create_client
11
11
  from letta.agent import Agent, save_agent
12
12
  from letta.config import LettaConfig
13
- from letta.constants import CLI_WARNING_PREFIX, LETTA_DIR
13
+ from letta.constants import CLI_WARNING_PREFIX, LETTA_DIR, MIN_CONTEXT_WINDOW
14
14
  from letta.local_llm.constants import ASSISTANT_MESSAGE_CLI_SYMBOL
15
15
  from letta.log import get_logger
16
16
  from letta.metadata import MetadataStore
@@ -244,6 +244,19 @@ def run(
244
244
  llm_model_name = questionary.select("Select LLM model:", choices=llm_choices).ask().model
245
245
  llm_config = [llm_config for llm_config in llm_configs if llm_config.model == llm_model_name][0]
246
246
 
247
+ # option to override context window
248
+ if llm_config.context_window is not None:
249
+ context_window_validator = lambda x: x.isdigit() and int(x) > MIN_CONTEXT_WINDOW and int(x) <= llm_config.context_window
250
+ context_window_input = questionary.text(
251
+ "Select LLM context window limit (hit enter for default):",
252
+ default=str(llm_config.context_window),
253
+ validate=context_window_validator,
254
+ ).ask()
255
+ if context_window_input is not None:
256
+ llm_config.context_window = int(context_window_input)
257
+ else:
258
+ sys.exit(1)
259
+
247
260
  # choose form list of embedding configs
248
261
  embedding_configs = client.list_embedding_configs()
249
262
  embedding_options = [embedding_config.embedding_model for embedding_config in embedding_configs]
letta/client/client.py CHANGED
@@ -77,6 +77,7 @@ class AbstractClient(object):
77
77
  memory: Memory = ChatMemory(human=get_human_text(DEFAULT_HUMAN), persona=get_persona_text(DEFAULT_PERSONA)),
78
78
  system: Optional[str] = None,
79
79
  tools: Optional[List[str]] = None,
80
+ tool_rules: Optional[List[BaseToolRule]] = None,
80
81
  include_base_tools: Optional[bool] = True,
81
82
  metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
82
83
  description: Optional[str] = None,
@@ -333,8 +334,12 @@ class RESTClient(AbstractClient):
333
334
  self._default_llm_config = default_llm_config
334
335
  self._default_embedding_config = default_embedding_config
335
336
 
336
- def list_agents(self) -> List[AgentState]:
337
- response = requests.get(f"{self.base_url}/{self.api_prefix}/agents", headers=self.headers)
337
+ def list_agents(self, tags: Optional[List[str]] = None) -> List[AgentState]:
338
+ params = {}
339
+ if tags:
340
+ params["tags"] = tags
341
+
342
+ response = requests.get(f"{self.base_url}/{self.api_prefix}/agents", headers=self.headers, params=params)
338
343
  return [AgentState(**agent) for agent in response.json()]
339
344
 
340
345
  def agent_exists(self, agent_id: str) -> bool:
@@ -372,6 +377,7 @@ class RESTClient(AbstractClient):
372
377
  system: Optional[str] = None,
373
378
  # tools
374
379
  tools: Optional[List[str]] = None,
380
+ tool_rules: Optional[List[BaseToolRule]] = None,
375
381
  include_base_tools: Optional[bool] = True,
376
382
  # metadata
377
383
  metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
@@ -425,6 +431,7 @@ class RESTClient(AbstractClient):
425
431
  metadata_=metadata,
426
432
  memory=memory,
427
433
  tools=tool_names,
434
+ tool_rules=tool_rules,
428
435
  system=system,
429
436
  agent_type=agent_type,
430
437
  llm_config=llm_config if llm_config else self._default_llm_config,
@@ -477,6 +484,7 @@ class RESTClient(AbstractClient):
477
484
  description: Optional[str] = None,
478
485
  system: Optional[str] = None,
479
486
  tools: Optional[List[str]] = None,
487
+ tags: Optional[List[str]] = None,
480
488
  metadata: Optional[Dict] = None,
481
489
  llm_config: Optional[LLMConfig] = None,
482
490
  embedding_config: Optional[EmbeddingConfig] = None,
@@ -506,6 +514,7 @@ class RESTClient(AbstractClient):
506
514
  name=name,
507
515
  system=system,
508
516
  tools=tools,
517
+ tags=tags,
509
518
  description=description,
510
519
  metadata_=metadata,
511
520
  llm_config=llm_config,
@@ -1614,13 +1623,10 @@ class LocalClient(AbstractClient):
1614
1623
  self.organization = self.server.get_organization_or_default(self.org_id)
1615
1624
 
1616
1625
  # agents
1617
- def list_agents(self) -> List[AgentState]:
1626
+ def list_agents(self, tags: Optional[List[str]] = None) -> List[AgentState]:
1618
1627
  self.interface.clear()
1619
1628
 
1620
- # TODO: fix the server function
1621
- # return self.server.list_agents(user_id=self.user_id)
1622
-
1623
- return self.server.ms.list_agents(user_id=self.user_id)
1629
+ return self.server.list_agents(user_id=self.user_id, tags=tags)
1624
1630
 
1625
1631
  def agent_exists(self, agent_id: Optional[str] = None, agent_name: Optional[str] = None) -> bool:
1626
1632
  """
@@ -1754,6 +1760,7 @@ class LocalClient(AbstractClient):
1754
1760
  description: Optional[str] = None,
1755
1761
  system: Optional[str] = None,
1756
1762
  tools: Optional[List[str]] = None,
1763
+ tags: Optional[List[str]] = None,
1757
1764
  metadata: Optional[Dict] = None,
1758
1765
  llm_config: Optional[LLMConfig] = None,
1759
1766
  embedding_config: Optional[EmbeddingConfig] = None,
@@ -1785,6 +1792,7 @@ class LocalClient(AbstractClient):
1785
1792
  name=name,
1786
1793
  system=system,
1787
1794
  tools=tools,
1795
+ tags=tags,
1788
1796
  description=description,
1789
1797
  metadata_=metadata,
1790
1798
  llm_config=llm_config,
@@ -1869,7 +1877,7 @@ class LocalClient(AbstractClient):
1869
1877
  agent_state (AgentState): State of the agent
1870
1878
  """
1871
1879
  self.interface.clear()
1872
- return self.server.get_agent(agent_name=agent_name, user_id=self.user_id, agent_id=None)
1880
+ return self.server.get_agent_state(agent_name=agent_name, user_id=self.user_id, agent_id=None)
1873
1881
 
1874
1882
  def get_agent(self, agent_id: str) -> AgentState:
1875
1883
  """
letta/constants.py CHANGED
@@ -18,6 +18,9 @@ IN_CONTEXT_MEMORY_KEYWORD = "CORE_MEMORY"
18
18
  # OpenAI error message: Invalid 'messages[1].tool_calls[0].id': string too long. Expected a string with maximum length 29, but got a string with length 36 instead.
19
19
  TOOL_CALL_ID_MAX_LEN = 29
20
20
 
21
+ # minimum context window size
22
+ MIN_CONTEXT_WINDOW = 4000
23
+
21
24
  # embeddings
22
25
  MAX_EMBEDDING_DIM = 4096 # maximum supported embeding size - do NOT change or else DBs will need to be reset
23
26
 
@@ -9,7 +9,7 @@ from letta.constants import CLI_WARNING_PREFIX
9
9
  from letta.functions.schema_generator import generate_schema
10
10
 
11
11
 
12
- def derive_openai_json_schema(source_code: str, name: Optional[str]) -> dict:
12
+ def derive_openai_json_schema(source_code: str, name: Optional[str] = None) -> dict:
13
13
  # auto-generate openai schema
14
14
  try:
15
15
  # Define a custom environment with necessary imports
@@ -1,4 +1,3 @@
1
- import warnings
2
1
  from typing import Dict, List, Optional, Set
3
2
 
4
3
  from pydantic import BaseModel, Field
@@ -67,7 +66,7 @@ class ToolRulesSolver(BaseModel):
67
66
  if error_on_empty:
68
67
  raise RuntimeError(message)
69
68
  else:
70
- warnings.warn(message)
69
+ # warnings.warn(message)
71
70
  return []
72
71
 
73
72
  def is_terminal_tool(self, tool_name: str) -> bool:
letta/log.py CHANGED
@@ -56,7 +56,7 @@ DEVELOPMENT_LOGGING = {
56
56
  "propagate": False,
57
57
  },
58
58
  "uvicorn": {
59
- "level": "INFO",
59
+ "level": "CRITICAL",
60
60
  "handlers": ["console"],
61
61
  "propagate": False,
62
62
  },
letta/main.py CHANGED
@@ -26,6 +26,9 @@ from letta.streaming_interface import AgentRefreshStreamingInterface
26
26
 
27
27
  # interface = interface()
28
28
 
29
+ # disable composio print on exit
30
+ os.environ["COMPOSIO_DISABLE_VERSION_CHECK"] = "true"
31
+
29
32
  app = typer.Typer(pretty_exceptions_enable=False)
30
33
  app.command(name="run")(run)
31
34
  app.command(name="version")(version)
letta/metadata.py CHANGED
@@ -493,6 +493,7 @@ class MetadataStore:
493
493
  fields = vars(agent)
494
494
  fields["memory"] = agent.memory.to_dict()
495
495
  del fields["_internal_memory"]
496
+ del fields["tags"]
496
497
  session.add(AgentModel(**fields))
497
498
  session.commit()
498
499
 
@@ -531,6 +532,7 @@ class MetadataStore:
531
532
  if isinstance(agent.memory, Memory): # TODO: this is nasty but this whole class will soon be removed so whatever
532
533
  fields["memory"] = agent.memory.to_dict()
533
534
  del fields["_internal_memory"]
535
+ del fields["tags"]
534
536
  session.query(AgentModel).filter(AgentModel.id == agent.id).update(fields)
535
537
  session.commit()
536
538
 
@@ -0,0 +1,28 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from sqlalchemy import ForeignKey, String, UniqueConstraint
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from letta.orm.mixins import OrganizationMixin
7
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
8
+ from letta.schemas.agents_tags import AgentsTags as PydanticAgentsTags
9
+
10
+ if TYPE_CHECKING:
11
+ from letta.orm.organization import Organization
12
+
13
+
14
+ class AgentsTags(SqlalchemyBase, OrganizationMixin):
15
+ """Associates tags with agents, allowing agents to have multiple tags and supporting tag-based filtering."""
16
+
17
+ __tablename__ = "agents_tags"
18
+ __pydantic_model__ = PydanticAgentsTags
19
+ __table_args__ = (UniqueConstraint("agent_id", "tag", name="unique_agent_tag"),)
20
+
21
+ # The agent associated with this tag
22
+ agent_id = mapped_column(String, ForeignKey("agents.id"), primary_key=True)
23
+
24
+ # The name of the tag
25
+ tag: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the tag associated with the agent.")
26
+
27
+ # relationships
28
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="agents_tags")
letta/orm/organization.py CHANGED
@@ -23,6 +23,7 @@ class Organization(SqlalchemyBase):
23
23
 
24
24
  users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
25
25
  tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
26
+ agents_tags: Mapped[List["AgentsTags"]] = relationship("AgentsTags", back_populates="organization", cascade="all, delete-orphan")
26
27
 
27
28
  # TODO: Map these relationships later when we actually make these models
28
29
  # below is just a suggestion
@@ -112,6 +112,22 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
112
112
  self.is_deleted = True
113
113
  return self.update(db_session)
114
114
 
115
+ def hard_delete(self, db_session: "Session", actor: Optional["User"] = None) -> None:
116
+ """Permanently removes the record from the database."""
117
+ if actor:
118
+ logger.info(f"User {actor.id} requested hard deletion of {self.__class__.__name__} with ID {self.id}")
119
+
120
+ with db_session as session:
121
+ try:
122
+ session.delete(self)
123
+ session.commit()
124
+ except Exception as e:
125
+ session.rollback()
126
+ logger.exception(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}")
127
+ raise ValueError(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}: {e}")
128
+ else:
129
+ logger.info(f"{self.__class__.__name__} with ID {self.id} successfully hard deleted")
130
+
115
131
  def update(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
116
132
  if actor:
117
133
  self._set_created_and_updated_by_fields(actor.id)
letta/schemas/agent.py CHANGED
@@ -64,6 +64,9 @@ class AgentState(BaseAgent, validate_assignment=True):
64
64
  # tool rules
65
65
  tool_rules: Optional[List[BaseToolRule]] = Field(default=None, description="The list of tool rules.")
66
66
 
67
+ # tags
68
+ tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
69
+
67
70
  # system prompt
68
71
  system: str = Field(..., description="The system prompt used by the agent.")
69
72
 
@@ -108,6 +111,7 @@ class CreateAgent(BaseAgent):
108
111
  memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
109
112
  tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
110
113
  tool_rules: Optional[List[BaseToolRule]] = Field(None, description="The tool rules governing the agent.")
114
+ tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
111
115
  system: Optional[str] = Field(None, description="The system prompt used by the agent.")
112
116
  agent_type: Optional[AgentType] = Field(None, description="The type of agent.")
113
117
  llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
@@ -148,6 +152,7 @@ class UpdateAgentState(BaseAgent):
148
152
  id: str = Field(..., description="The id of the agent.")
149
153
  name: Optional[str] = Field(None, description="The name of the agent.")
150
154
  tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
155
+ tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
151
156
  system: Optional[str] = Field(None, description="The system prompt used by the agent.")
152
157
  llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
153
158
  embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the agent.")
@@ -0,0 +1,33 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from pydantic import Field
5
+
6
+ from letta.schemas.letta_base import LettaBase
7
+
8
+
9
+ class AgentsTagsBase(LettaBase):
10
+ __id_prefix__ = "agents_tags"
11
+
12
+
13
+ class AgentsTags(AgentsTagsBase):
14
+ """
15
+ Schema representing the relationship between tags and agents.
16
+
17
+ Parameters:
18
+ agent_id (str): The ID of the associated agent.
19
+ tag_id (str): The ID of the associated tag.
20
+ tag_name (str): The name of the tag.
21
+ created_at (datetime): The date this relationship was created.
22
+ """
23
+
24
+ id: str = AgentsTagsBase.generate_id_field()
25
+ agent_id: str = Field(..., description="The ID of the associated agent.")
26
+ tag: str = Field(..., description="The name of the tag.")
27
+ created_at: Optional[datetime] = Field(None, description="The creation date of the association.")
28
+ updated_at: Optional[datetime] = Field(None, description="The update date of the tag.")
29
+ is_deleted: bool = Field(False, description="Whether this tag is deleted or not.")
30
+
31
+
32
+ class AgentsTagsCreate(AgentsTagsBase):
33
+ tag: str = Field(..., description="The tag name.")
letta/schemas/block.py CHANGED
@@ -18,7 +18,7 @@ class BaseBlock(LettaBase, validate_assignment=True):
18
18
  limit: int = Field(2000, description="Character limit of the block.")
19
19
 
20
20
  # template data (optional)
21
- template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
21
+ template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
22
22
  template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
23
23
 
24
24
  # context window label
@@ -58,11 +58,11 @@ class Block(BaseBlock):
58
58
  A Block represents a reserved section of the LLM's context window which is editable. `Block` objects contained in the `Memory` object, which is able to edit the Block values.
59
59
 
60
60
  Parameters:
61
- name (str): The name of the block.
61
+ label (str): The label of the block (e.g. 'human', 'persona'). This defines a category for the block.
62
62
  value (str): The value of the block. This is the string that is represented in the context window.
63
63
  limit (int): The character limit of the block.
64
+ template_name (str): The name of the block template (if it is a template).
64
65
  template (bool): Whether the block is a template (e.g. saved human/persona options). Non-template blocks are not stored in the database and are ephemeral, while templated blocks are stored in the database.
65
- label (str): The label of the block (e.g. 'human', 'persona'). This defines a category for the block.
66
66
  description (str): Description of the block.
67
67
  metadata_ (Dict): Metadata of the block.
68
68
  user_id (str): The unique identifier of the user associated with the block.
@@ -1,3 +1,6 @@
1
+ import html
2
+ import json
3
+ import re
1
4
  from typing import List, Union
2
5
 
3
6
  from pydantic import BaseModel, Field
@@ -34,6 +37,113 @@ class LettaResponse(BaseModel):
34
37
  indent=4,
35
38
  )
36
39
 
40
+ def _repr_html_(self):
41
+ def get_formatted_content(msg):
42
+ if msg.message_type == "internal_monologue":
43
+ return f'<div class="content"><span class="internal-monologue">{html.escape(msg.internal_monologue)}</span></div>'
44
+ elif msg.message_type == "function_call":
45
+ args = format_json(msg.function_call.arguments)
46
+ return f'<div class="content"><span class="function-name">{html.escape(msg.function_call.name)}</span>({args})</div>'
47
+ elif msg.message_type == "function_return":
48
+
49
+ return_value = format_json(msg.function_return)
50
+ # return f'<div class="status-line">Status: {html.escape(msg.status)}</div><div class="content">{return_value}</div>'
51
+ return f'<div class="content">{return_value}</div>'
52
+ elif msg.message_type == "user_message":
53
+ if is_json(msg.message):
54
+ return f'<div class="content">{format_json(msg.message)}</div>'
55
+ else:
56
+ return f'<div class="content">{html.escape(msg.message)}</div>'
57
+ elif msg.message_type in ["assistant_message", "system_message"]:
58
+ return f'<div class="content">{html.escape(msg.message)}</div>'
59
+ else:
60
+ return f'<div class="content">{html.escape(str(msg))}</div>'
61
+
62
+ def is_json(string):
63
+ try:
64
+ json.loads(string)
65
+ return True
66
+ except ValueError:
67
+ return False
68
+
69
+ def format_json(json_str):
70
+ try:
71
+ parsed = json.loads(json_str)
72
+ formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
73
+ formatted = formatted.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
74
+ formatted = formatted.replace("\n", "<br>").replace(" ", "&nbsp;&nbsp;")
75
+ formatted = re.sub(r'(".*?"):', r'<span class="json-key">\1</span>:', formatted)
76
+ formatted = re.sub(r': (".*?")', r': <span class="json-string">\1</span>', formatted)
77
+ formatted = re.sub(r": (\d+)", r': <span class="json-number">\1</span>', formatted)
78
+ formatted = re.sub(r": (true|false)", r': <span class="json-boolean">\1</span>', formatted)
79
+ return formatted
80
+ except json.JSONDecodeError:
81
+ return html.escape(json_str)
82
+
83
+ html_output = """
84
+ <style>
85
+ .message-container, .usage-container {
86
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
87
+ max-width: 800px;
88
+ margin: 20px auto;
89
+ background-color: #1e1e1e;
90
+ border-radius: 8px;
91
+ overflow: hidden;
92
+ color: #d4d4d4;
93
+ }
94
+ .message, .usage-stats {
95
+ padding: 10px 15px;
96
+ border-bottom: 1px solid #3a3a3a;
97
+ }
98
+ .message:last-child, .usage-stats:last-child {
99
+ border-bottom: none;
100
+ }
101
+ .title {
102
+ font-weight: bold;
103
+ margin-bottom: 5px;
104
+ color: #ffffff;
105
+ text-transform: uppercase;
106
+ font-size: 0.9em;
107
+ }
108
+ .content {
109
+ background-color: #2d2d2d;
110
+ border-radius: 4px;
111
+ padding: 5px 10px;
112
+ font-family: 'Consolas', 'Courier New', monospace;
113
+ white-space: pre-wrap;
114
+ }
115
+ .json-key, .function-name, .json-boolean { color: #9cdcfe; }
116
+ .json-string { color: #ce9178; }
117
+ .json-number { color: #b5cea8; }
118
+ .internal-monologue { font-style: italic; }
119
+ </style>
120
+ <div class="message-container">
121
+ """
122
+
123
+ for msg in self.messages:
124
+ content = get_formatted_content(msg)
125
+ title = msg.message_type.replace("_", " ").upper()
126
+ html_output += f"""
127
+ <div class="message">
128
+ <div class="title">{title}</div>
129
+ {content}
130
+ </div>
131
+ """
132
+ html_output += "</div>"
133
+
134
+ # Formatting the usage statistics
135
+ usage_html = json.dumps(self.usage.model_dump(), indent=2)
136
+ html_output += f"""
137
+ <div class="usage-container">
138
+ <div class="usage-stats">
139
+ <div class="title">USAGE STATISTICS</div>
140
+ <div class="content">{format_json(usage_html)}</div>
141
+ </div>
142
+ </div>
143
+ """
144
+
145
+ return html_output
146
+
37
147
 
38
148
  # The streaming response is either [DONE], [DONE_STEP], [DONE], an error, or a LettaMessage
39
149
  LettaStreamingResponse = Union[LettaMessage, MessageStreamStatus, LettaUsageStatistics]
@@ -13,7 +13,7 @@ class LLMConfig(BaseModel):
13
13
  model_endpoint (str): The endpoint for the model.
14
14
  model_wrapper (str): The wrapper for the model. This is used to wrap additional text around the input/output of the model. This is useful for text-to-text completions, such as the Completions API in OpenAI.
15
15
  context_window (int): The context window size for the model.
16
- put_inner_thoughts_in_kwargs (bool): Puts 'inner_thoughts' as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.
16
+ put_inner_thoughts_in_kwargs (bool): Puts `inner_thoughts` as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.
17
17
  """
18
18
 
19
19
  # TODO: 🤮 don't default to a vendor! bug city!
@@ -67,6 +67,12 @@ class LLMConfig(BaseModel):
67
67
 
68
68
  @classmethod
69
69
  def default_config(cls, model_name: str):
70
+ """
71
+ Convinience function to generate a default `LLMConfig` from a model name. Only some models are supported in this function.
72
+
73
+ Args:
74
+ model_name (str): The name of the model (gpt-4, gpt-4o-mini, letta).
75
+ """
70
76
  if model_name == "gpt-4":
71
77
  return cls(
72
78
  model="gpt-4",
letta/schemas/tool.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
- from composio import LogLevel
4
3
  from pydantic import Field
5
4
 
6
5
  from letta.functions.helpers import (
@@ -68,7 +67,7 @@ class ToolCreate(LettaBase):
68
67
  tags: List[str] = Field([], description="Metadata tags.")
69
68
  module: Optional[str] = Field(None, description="The source code of the function.")
70
69
  source_code: str = Field(..., description="The source code of the function.")
71
- source_type: str = Field(..., description="The source type of the function.")
70
+ source_type: str = Field("python", description="The source type of the function.")
72
71
  json_schema: Optional[Dict] = Field(
73
72
  None, description="The JSON schema of the function (auto-generated from source_code if not provided)"
74
73
  )
@@ -86,6 +85,7 @@ class ToolCreate(LettaBase):
86
85
  Returns:
87
86
  Tool: A Letta Tool initialized with attributes derived from the Composio tool.
88
87
  """
88
+ from composio import LogLevel
89
89
  from composio_langchain import ComposioToolSet
90
90
 
91
91
  composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR)
@@ -216,3 +216,7 @@ class ToolUpdate(LettaBase):
216
216
  json_schema: Optional[Dict] = Field(
217
217
  None, description="The JSON schema of the function (auto-generated from source_code if not provided)"
218
218
  )
219
+
220
+ class Config:
221
+ extra = "ignore" # Allows extra fields without validation errors
222
+ # TODO: Remove this, and clean usage of ToolUpdate everywhere else
@@ -11,15 +11,25 @@ class BaseToolRule(LettaBase):
11
11
 
12
12
 
13
13
  class ToolRule(BaseToolRule):
14
+ """
15
+ A ToolRule represents a tool that can be invoked by the agent.
16
+ """
17
+
14
18
  type: str = Field("ToolRule")
15
19
  children: List[str] = Field(..., description="The children tools that can be invoked.")
16
20
 
17
21
 
18
22
  class InitToolRule(BaseToolRule):
23
+ """
24
+ Represents the initial tool rule configuration.
25
+ """
26
+
19
27
  type: str = Field("InitToolRule")
20
- """Represents the initial tool rule configuration."""
21
28
 
22
29
 
23
30
  class TerminalToolRule(BaseToolRule):
31
+ """
32
+ Represents a terminal tool rule configuration where if this tool gets called, it must end the agent loop.
33
+ """
34
+
24
35
  type: str = Field("TerminalToolRule")
25
- """Represents a terminal tool rule configuration where if this tool gets called, it must end the agent loop."""
@@ -8,6 +8,7 @@ import uvicorn
8
8
  from fastapi import FastAPI
9
9
  from starlette.middleware.cors import CORSMiddleware
10
10
 
11
+ from letta.__init__ import __version__
11
12
  from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
12
13
  from letta.schemas.letta_response import LettaResponse
13
14
  from letta.server.constants import REST_DEFAULT_PORT
@@ -66,6 +67,7 @@ def create_application() -> "FastAPI":
66
67
  """the application start routine"""
67
68
  # global server
68
69
  # server = SyncServer(default_interface_factory=lambda: interface())
70
+ print(f"\n[[ Letta server // v{__version__} ]]")
69
71
 
70
72
  app = FastAPI(
71
73
  swagger_ui_parameters={"docExpansion": "none"},
@@ -78,6 +80,7 @@ def create_application() -> "FastAPI":
78
80
 
79
81
  if "--ade" in sys.argv:
80
82
  settings.cors_origins.append("https://app.letta.com")
83
+ print(f"▶ View using ADE at: https://app.letta.com/local-project/agents")
81
84
 
82
85
  app.add_middleware(
83
86
  CORSMiddleware,
@@ -179,7 +182,7 @@ def start_server(
179
182
  # Add the handler to the logger
180
183
  server_logger.addHandler(stream_handler)
181
184
 
182
- print(f"Running: uvicorn server:app --host {host or 'localhost'} --port {port or REST_DEFAULT_PORT}")
185
+ print(f" Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}\n")
183
186
  uvicorn.run(
184
187
  app,
185
188
  host=host or "localhost",