letta-nightly 0.5.4.dev20241128000451__py3-none-any.whl → 0.6.0.dev20241204051808__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.
- letta/__init__.py +1 -1
- letta/agent.py +45 -44
- letta/cli/cli.py +9 -3
- letta/client/client.py +6 -1
- letta/functions/functions.py +40 -55
- letta/functions/schema_generator.py +269 -29
- letta/llm_api/helpers.py +99 -5
- letta/llm_api/openai.py +8 -2
- letta/local_llm/utils.py +8 -4
- letta/metadata.py +4 -5
- letta/schemas/block.py +4 -3
- letta/schemas/tool.py +12 -0
- letta/server/rest_api/app.py +5 -4
- letta/server/rest_api/routers/v1/agents.py +24 -30
- letta/server/rest_api/routers/v1/sandbox_configs.py +19 -0
- letta/server/rest_api/routers/v1/sources.py +8 -1
- letta/server/rest_api/routers/v1/tools.py +88 -1
- letta/server/server.py +146 -12
- letta/services/per_agent_lock_manager.py +3 -3
- letta/services/tool_execution_sandbox.py +50 -24
- letta/utils.py +0 -7
- {letta_nightly-0.5.4.dev20241128000451.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/METADATA +6 -6
- {letta_nightly-0.5.4.dev20241128000451.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/RECORD +26 -26
- {letta_nightly-0.5.4.dev20241128000451.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.4.dev20241128000451.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.4.dev20241128000451.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/entry_points.txt +0 -0
letta/llm_api/helpers.py
CHANGED
|
@@ -11,7 +11,55 @@ from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
|
|
11
11
|
from letta.utils import json_dumps, printd
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def _convert_to_structured_output_helper(property: dict) -> dict:
|
|
15
|
+
"""Convert a single JSON schema property to structured output format (recursive)"""
|
|
16
|
+
|
|
17
|
+
if "type" not in property:
|
|
18
|
+
raise ValueError(f"Property {property} is missing a type")
|
|
19
|
+
param_type = property["type"]
|
|
20
|
+
|
|
21
|
+
if "description" not in property:
|
|
22
|
+
# raise ValueError(f"Property {property} is missing a description")
|
|
23
|
+
param_description = None
|
|
24
|
+
else:
|
|
25
|
+
param_description = property["description"]
|
|
26
|
+
|
|
27
|
+
if param_type == "object":
|
|
28
|
+
if "properties" not in property:
|
|
29
|
+
raise ValueError(f"Property {property} of type object is missing properties")
|
|
30
|
+
properties = property["properties"]
|
|
31
|
+
property_dict = {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {k: _convert_to_structured_output_helper(v) for k, v in properties.items()},
|
|
34
|
+
"additionalProperties": False,
|
|
35
|
+
"required": list(properties.keys()),
|
|
36
|
+
}
|
|
37
|
+
if param_description is not None:
|
|
38
|
+
property_dict["description"] = param_description
|
|
39
|
+
return property_dict
|
|
40
|
+
|
|
41
|
+
elif param_type == "array":
|
|
42
|
+
if "items" not in property:
|
|
43
|
+
raise ValueError(f"Property {property} of type array is missing items")
|
|
44
|
+
items = property["items"]
|
|
45
|
+
property_dict = {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": _convert_to_structured_output_helper(items),
|
|
48
|
+
}
|
|
49
|
+
if param_description is not None:
|
|
50
|
+
property_dict["description"] = param_description
|
|
51
|
+
return property_dict
|
|
52
|
+
|
|
53
|
+
else:
|
|
54
|
+
property_dict = {
|
|
55
|
+
"type": param_type, # simple type
|
|
56
|
+
}
|
|
57
|
+
if param_description is not None:
|
|
58
|
+
property_dict["description"] = param_description
|
|
59
|
+
return property_dict
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def convert_to_structured_output(openai_function: dict, allow_optional: bool = False) -> dict:
|
|
15
63
|
"""Convert function call objects to structured output objects
|
|
16
64
|
|
|
17
65
|
See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas
|
|
@@ -22,17 +70,63 @@ def convert_to_structured_output(openai_function: dict) -> dict:
|
|
|
22
70
|
"name": openai_function["name"],
|
|
23
71
|
"description": description,
|
|
24
72
|
"strict": True,
|
|
25
|
-
"parameters": {
|
|
73
|
+
"parameters": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {},
|
|
76
|
+
"additionalProperties": False,
|
|
77
|
+
"required": [],
|
|
78
|
+
},
|
|
26
79
|
}
|
|
27
80
|
|
|
81
|
+
# This code needs to be able to handle nested properties
|
|
82
|
+
# For example, the param details may have "type" + "description",
|
|
83
|
+
# but if "type" is "object" we expected "properties", where each property has details
|
|
84
|
+
# and if "type" is "array" we expect "items": <type>
|
|
28
85
|
for param, details in openai_function["parameters"]["properties"].items():
|
|
29
|
-
|
|
86
|
+
|
|
87
|
+
param_type = details["type"]
|
|
88
|
+
description = details["description"]
|
|
89
|
+
|
|
90
|
+
if param_type == "object":
|
|
91
|
+
if "properties" not in details:
|
|
92
|
+
# Structured outputs requires the properties on dicts be specified ahead of time
|
|
93
|
+
raise ValueError(f"Property {param} of type object is missing properties")
|
|
94
|
+
structured_output["parameters"]["properties"][param] = {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"description": description,
|
|
97
|
+
"properties": {k: _convert_to_structured_output_helper(v) for k, v in details["properties"].items()},
|
|
98
|
+
"additionalProperties": False,
|
|
99
|
+
"required": list(details["properties"].keys()),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
elif param_type == "array":
|
|
103
|
+
structured_output["parameters"]["properties"][param] = {
|
|
104
|
+
"type": "array",
|
|
105
|
+
"description": description,
|
|
106
|
+
"items": _convert_to_structured_output_helper(details["items"]),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
structured_output["parameters"]["properties"][param] = {
|
|
111
|
+
"type": param_type, # simple type
|
|
112
|
+
"description": description,
|
|
113
|
+
}
|
|
30
114
|
|
|
31
115
|
if "enum" in details:
|
|
32
116
|
structured_output["parameters"]["properties"][param]["enum"] = details["enum"]
|
|
33
117
|
|
|
34
|
-
|
|
35
|
-
|
|
118
|
+
if not allow_optional:
|
|
119
|
+
# Add all properties to required list
|
|
120
|
+
structured_output["parameters"]["required"] = list(structured_output["parameters"]["properties"].keys())
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
# See what parameters exist that aren't required
|
|
124
|
+
# Those are implied "optional" types
|
|
125
|
+
# For those types, turn each of them into a union type with "null"
|
|
126
|
+
# e.g.
|
|
127
|
+
# "type": "string" -> "type": ["string", "null"]
|
|
128
|
+
# TODO
|
|
129
|
+
raise NotImplementedError
|
|
36
130
|
|
|
37
131
|
return structured_output
|
|
38
132
|
|
letta/llm_api/openai.py
CHANGED
|
@@ -477,7 +477,10 @@ def openai_chat_completions_request_stream(
|
|
|
477
477
|
if "tools" in data:
|
|
478
478
|
for tool in data["tools"]:
|
|
479
479
|
# tool["strict"] = True
|
|
480
|
-
|
|
480
|
+
try:
|
|
481
|
+
tool["function"] = convert_to_structured_output(tool["function"])
|
|
482
|
+
except ValueError as e:
|
|
483
|
+
warnings.warn(f"Failed to convert tool function to structured output, tool={tool}, error={e}")
|
|
481
484
|
|
|
482
485
|
# print(f"\n\n\n\nData[tools]: {json.dumps(data['tools'], indent=2)}")
|
|
483
486
|
|
|
@@ -533,7 +536,10 @@ def openai_chat_completions_request(
|
|
|
533
536
|
|
|
534
537
|
if "tools" in data:
|
|
535
538
|
for tool in data["tools"]:
|
|
536
|
-
|
|
539
|
+
try:
|
|
540
|
+
tool["function"] = convert_to_structured_output(tool["function"])
|
|
541
|
+
except ValueError as e:
|
|
542
|
+
warnings.warn(f"Failed to convert tool function to structured output, tool={tool}, error={e}")
|
|
537
543
|
|
|
538
544
|
response_json = make_post_request(url, headers, data)
|
|
539
545
|
return ChatCompletionResponse(**response_json)
|
letta/local_llm/utils.py
CHANGED
|
@@ -88,7 +88,9 @@ def num_tokens_from_functions(functions: List[dict], model: str = "gpt-4"):
|
|
|
88
88
|
try:
|
|
89
89
|
encoding = tiktoken.encoding_for_model(model)
|
|
90
90
|
except KeyError:
|
|
91
|
-
|
|
91
|
+
from letta.utils import printd
|
|
92
|
+
|
|
93
|
+
printd(f"Warning: model not found. Using cl100k_base encoding.")
|
|
92
94
|
encoding = tiktoken.get_encoding("cl100k_base")
|
|
93
95
|
|
|
94
96
|
num_tokens = 0
|
|
@@ -121,7 +123,7 @@ def num_tokens_from_functions(functions: List[dict], model: str = "gpt-4"):
|
|
|
121
123
|
function_tokens += 3
|
|
122
124
|
function_tokens += len(encoding.encode(o))
|
|
123
125
|
else:
|
|
124
|
-
|
|
126
|
+
warnings.warn(f"num_tokens_from_functions: Unsupported field {field} in function {function}")
|
|
125
127
|
function_tokens += 11
|
|
126
128
|
|
|
127
129
|
num_tokens += function_tokens
|
|
@@ -215,8 +217,10 @@ def num_tokens_from_messages(messages: List[dict], model: str = "gpt-4") -> int:
|
|
|
215
217
|
# print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
|
|
216
218
|
return num_tokens_from_messages(messages, model="gpt-4-0613")
|
|
217
219
|
else:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
from letta.utils import printd
|
|
221
|
+
|
|
222
|
+
printd(
|
|
223
|
+
f"num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens."
|
|
220
224
|
)
|
|
221
225
|
return num_tokens_from_messages(messages, model="gpt-4-0613")
|
|
222
226
|
# raise NotImplementedError(
|
letta/metadata.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import secrets
|
|
5
|
-
import warnings
|
|
6
5
|
from typing import List, Optional, Union
|
|
7
6
|
|
|
8
7
|
from sqlalchemy import JSON, Column, DateTime, Index, String, TypeDecorator
|
|
@@ -359,8 +358,8 @@ class MetadataStore:
|
|
|
359
358
|
# warnings.warn(f"Agent {agent.id} has no _internal_memory field")
|
|
360
359
|
if "tags" in fields:
|
|
361
360
|
del fields["tags"]
|
|
362
|
-
else:
|
|
363
|
-
|
|
361
|
+
# else:
|
|
362
|
+
# warnings.warn(f"Agent {agent.id} has no tags field")
|
|
364
363
|
session.add(AgentModel(**fields))
|
|
365
364
|
session.commit()
|
|
366
365
|
|
|
@@ -376,8 +375,8 @@ class MetadataStore:
|
|
|
376
375
|
# warnings.warn(f"Agent {agent.id} has no _internal_memory field")
|
|
377
376
|
if "tags" in fields:
|
|
378
377
|
del fields["tags"]
|
|
379
|
-
else:
|
|
380
|
-
|
|
378
|
+
# else:
|
|
379
|
+
# warnings.warn(f"Agent {agent.id} has no tags field")
|
|
381
380
|
session.query(AgentModel).filter(AgentModel.id == agent.id).update(fields)
|
|
382
381
|
session.commit()
|
|
383
382
|
|
letta/schemas/block.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Optional
|
|
|
3
3
|
from pydantic import BaseModel, Field, model_validator
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
|
+
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
|
6
7
|
from letta.schemas.letta_base import LettaBase
|
|
7
8
|
|
|
8
9
|
# block of the LLM context
|
|
@@ -15,7 +16,7 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
15
16
|
|
|
16
17
|
# data value
|
|
17
18
|
value: str = Field(..., description="Value of the block.")
|
|
18
|
-
limit: int = Field(
|
|
19
|
+
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
|
|
19
20
|
|
|
20
21
|
# template data (optional)
|
|
21
22
|
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
|
|
@@ -117,7 +118,7 @@ class BlockLabelUpdate(BaseModel):
|
|
|
117
118
|
class BlockUpdate(BaseBlock):
|
|
118
119
|
"""Update a block"""
|
|
119
120
|
|
|
120
|
-
limit: Optional[int] = Field(
|
|
121
|
+
limit: Optional[int] = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
|
|
121
122
|
value: Optional[str] = Field(None, description="Value of the block.")
|
|
122
123
|
|
|
123
124
|
class Config:
|
|
@@ -147,7 +148,7 @@ class CreateBlock(BaseBlock):
|
|
|
147
148
|
"""Create a block"""
|
|
148
149
|
|
|
149
150
|
label: str = Field(..., description="Label of the block.")
|
|
150
|
-
limit: int = Field(
|
|
151
|
+
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
|
|
151
152
|
value: str = Field(..., description="Value of the block.")
|
|
152
153
|
|
|
153
154
|
# block templates
|
letta/schemas/tool.py
CHANGED
|
@@ -201,3 +201,15 @@ class ToolUpdate(LettaBase):
|
|
|
201
201
|
class Config:
|
|
202
202
|
extra = "ignore" # Allows extra fields without validation errors
|
|
203
203
|
# TODO: Remove this, and clean usage of ToolUpdate everywhere else
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ToolRun(LettaBase):
|
|
207
|
+
id: str = Field(..., description="The ID of the tool to run.")
|
|
208
|
+
args: str = Field(..., description="The arguments to pass to the tool (as stringified JSON).")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ToolRunFromSource(LettaBase):
|
|
212
|
+
source_code: str = Field(..., description="The source code of the function.")
|
|
213
|
+
args: str = Field(..., description="The arguments to pass to the tool (as stringified JSON).")
|
|
214
|
+
name: Optional[str] = Field(None, description="The name of the tool to run.")
|
|
215
|
+
source_type: Optional[str] = Field(None, description="The type of the source code.")
|
letta/server/rest_api/app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
@@ -103,7 +104,7 @@ def generate_password():
|
|
|
103
104
|
return secrets.token_urlsafe(16)
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
random_password = generate_password()
|
|
107
|
+
random_password = os.getenv("LETTA_SERVER_PASSWORD") or generate_password()
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
class CheckPasswordMiddleware(BaseHTTPMiddleware):
|
|
@@ -132,11 +133,11 @@ def create_application() -> "FastAPI":
|
|
|
132
133
|
debug=True,
|
|
133
134
|
)
|
|
134
135
|
|
|
135
|
-
if "--ade" in sys.argv:
|
|
136
|
+
if (os.getenv("LETTA_SERVER_ADE") == "true") or "--ade" in sys.argv:
|
|
136
137
|
settings.cors_origins.append("https://app.letta.com")
|
|
137
|
-
print(f"▶ View using ADE at: https://app.letta.com/
|
|
138
|
+
print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard")
|
|
138
139
|
|
|
139
|
-
if "--secure" in sys.argv:
|
|
140
|
+
if (os.getenv("LETTA_SERVER_SECURE") == "true") or "--secure" in sys.argv:
|
|
140
141
|
print(f"▶ Using secure mode with password: {random_password}")
|
|
141
142
|
app.add_middleware(CheckPasswordMiddleware)
|
|
142
143
|
|
|
@@ -448,21 +448,18 @@ async def send_message(
|
|
|
448
448
|
This endpoint accepts a message from a user and processes it through the agent.
|
|
449
449
|
"""
|
|
450
450
|
actor = server.get_user_or_default(user_id=user_id)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
|
464
|
-
)
|
|
465
|
-
return result
|
|
451
|
+
result = await send_message_to_agent(
|
|
452
|
+
server=server,
|
|
453
|
+
agent_id=agent_id,
|
|
454
|
+
user_id=actor.id,
|
|
455
|
+
messages=request.messages,
|
|
456
|
+
stream_steps=False,
|
|
457
|
+
stream_tokens=False,
|
|
458
|
+
# Support for AssistantMessage
|
|
459
|
+
assistant_message_tool_name=request.assistant_message_tool_name,
|
|
460
|
+
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
|
461
|
+
)
|
|
462
|
+
return result
|
|
466
463
|
|
|
467
464
|
|
|
468
465
|
@router.post(
|
|
@@ -490,21 +487,18 @@ async def send_message_streaming(
|
|
|
490
487
|
It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
|
|
491
488
|
"""
|
|
492
489
|
actor = server.get_user_or_default(user_id=user_id)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
|
506
|
-
)
|
|
507
|
-
return result
|
|
490
|
+
result = await send_message_to_agent(
|
|
491
|
+
server=server,
|
|
492
|
+
agent_id=agent_id,
|
|
493
|
+
user_id=actor.id,
|
|
494
|
+
messages=request.messages,
|
|
495
|
+
stream_steps=True,
|
|
496
|
+
stream_tokens=request.stream_tokens,
|
|
497
|
+
# Support for AssistantMessage
|
|
498
|
+
assistant_message_tool_name=request.assistant_message_tool_name,
|
|
499
|
+
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
|
500
|
+
)
|
|
501
|
+
return result
|
|
508
502
|
|
|
509
503
|
|
|
510
504
|
# TODO: move this into server.py?
|
|
@@ -8,6 +8,7 @@ from letta.schemas.sandbox_config import SandboxEnvironmentVariable as PydanticE
|
|
|
8
8
|
from letta.schemas.sandbox_config import (
|
|
9
9
|
SandboxEnvironmentVariableCreate,
|
|
10
10
|
SandboxEnvironmentVariableUpdate,
|
|
11
|
+
SandboxType,
|
|
11
12
|
)
|
|
12
13
|
from letta.server.rest_api.utils import get_letta_server, get_user_id
|
|
13
14
|
from letta.server.server import SyncServer
|
|
@@ -29,6 +30,24 @@ def create_sandbox_config(
|
|
|
29
30
|
return server.sandbox_config_manager.create_or_update_sandbox_config(config_create, actor)
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@router.post("/e2b/default", response_model=PydanticSandboxConfig)
|
|
34
|
+
def create_default_e2b_sandbox_config(
|
|
35
|
+
server: SyncServer = Depends(get_letta_server),
|
|
36
|
+
user_id: str = Depends(get_user_id),
|
|
37
|
+
):
|
|
38
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
39
|
+
return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=actor)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.post("/local/default", response_model=PydanticSandboxConfig)
|
|
43
|
+
def create_default_local_sandbox_config(
|
|
44
|
+
server: SyncServer = Depends(get_letta_server),
|
|
45
|
+
user_id: str = Depends(get_user_id),
|
|
46
|
+
):
|
|
47
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
48
|
+
return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=actor)
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
@router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
|
|
33
52
|
def update_sandbox_config(
|
|
34
53
|
sandbox_config_id: str,
|
|
@@ -37,7 +37,10 @@ def get_source(
|
|
|
37
37
|
"""
|
|
38
38
|
actor = server.get_user_or_default(user_id=user_id)
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
source = server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
|
41
|
+
if not source:
|
|
42
|
+
raise HTTPException(status_code=404, detail=f"Source with id={source_id} not found.")
|
|
43
|
+
return source
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
@router.get("/name/{source_name}", response_model=str, operation_id="get_source_id_by_name")
|
|
@@ -52,6 +55,8 @@ def get_source_id_by_name(
|
|
|
52
55
|
actor = server.get_user_or_default(user_id=user_id)
|
|
53
56
|
|
|
54
57
|
source = server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
|
|
58
|
+
if not source:
|
|
59
|
+
raise HTTPException(status_code=404, detail=f"Source with name={source_name} not found.")
|
|
55
60
|
return source.id
|
|
56
61
|
|
|
57
62
|
|
|
@@ -94,6 +99,8 @@ def update_source(
|
|
|
94
99
|
Update the name or documentation of an existing data source.
|
|
95
100
|
"""
|
|
96
101
|
actor = server.get_user_or_default(user_id=user_id)
|
|
102
|
+
if not server.source_manager.get_source_by_id(source_id=source_id, actor=actor):
|
|
103
|
+
raise HTTPException(status_code=404, detail=f"Source with id={source_id} does not exist.")
|
|
97
104
|
return server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
|
|
98
105
|
|
|
99
106
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from typing import List, Optional
|
|
2
2
|
|
|
3
|
+
from composio.client.collections import ActionModel, AppModel
|
|
3
4
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException
|
|
4
5
|
|
|
5
6
|
from letta.errors import LettaToolCreateError
|
|
6
7
|
from letta.orm.errors import UniqueConstraintViolationError
|
|
7
|
-
from letta.schemas.
|
|
8
|
+
from letta.schemas.letta_message import FunctionReturn
|
|
9
|
+
from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
|
|
8
10
|
from letta.server.rest_api.utils import get_letta_server
|
|
9
11
|
from letta.server.server import SyncServer
|
|
10
12
|
|
|
@@ -156,3 +158,88 @@ def add_base_tools(
|
|
|
156
158
|
"""
|
|
157
159
|
actor = server.get_user_or_default(user_id=user_id)
|
|
158
160
|
return server.tool_manager.add_base_tools(actor=actor)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# NOTE: can re-enable if needed
|
|
164
|
+
# @router.post("/{tool_id}/run", response_model=FunctionReturn, operation_id="run_tool")
|
|
165
|
+
# def run_tool(
|
|
166
|
+
# server: SyncServer = Depends(get_letta_server),
|
|
167
|
+
# request: ToolRun = Body(...),
|
|
168
|
+
# user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
169
|
+
# ):
|
|
170
|
+
# """
|
|
171
|
+
# Run an existing tool on provided arguments
|
|
172
|
+
# """
|
|
173
|
+
# actor = server.get_user_or_default(user_id=user_id)
|
|
174
|
+
|
|
175
|
+
# return server.run_tool(tool_id=request.tool_id, tool_args=request.tool_args, user_id=actor.id)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@router.post("/run", response_model=FunctionReturn, operation_id="run_tool_from_source")
|
|
179
|
+
def run_tool_from_source(
|
|
180
|
+
server: SyncServer = Depends(get_letta_server),
|
|
181
|
+
request: ToolRunFromSource = Body(...),
|
|
182
|
+
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
|
183
|
+
):
|
|
184
|
+
"""
|
|
185
|
+
Attempt to build a tool from source, then run it on the provided arguments
|
|
186
|
+
"""
|
|
187
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
return server.run_tool_from_source(
|
|
191
|
+
tool_source=request.source_code,
|
|
192
|
+
tool_source_type=request.source_type,
|
|
193
|
+
tool_args=request.args,
|
|
194
|
+
tool_name=request.name,
|
|
195
|
+
user_id=actor.id,
|
|
196
|
+
)
|
|
197
|
+
except LettaToolCreateError as e:
|
|
198
|
+
# HTTP 400 == Bad Request
|
|
199
|
+
print(f"Error occurred during tool creation: {e}")
|
|
200
|
+
# print the full stack trace
|
|
201
|
+
import traceback
|
|
202
|
+
|
|
203
|
+
print(traceback.format_exc())
|
|
204
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
# Catch other unexpected errors and raise an internal server error
|
|
208
|
+
print(f"Unexpected error occurred: {e}")
|
|
209
|
+
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Specific routes for Composio
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@router.get("/composio/apps", response_model=List[AppModel], operation_id="list_composio_apps")
|
|
216
|
+
def list_composio_apps(server: SyncServer = Depends(get_letta_server)):
|
|
217
|
+
"""
|
|
218
|
+
Get a list of all Composio apps
|
|
219
|
+
"""
|
|
220
|
+
return server.get_composio_apps()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@router.get("/composio/apps/{composio_app_name}/actions", response_model=List[ActionModel], operation_id="list_composio_actions_by_app")
|
|
224
|
+
def list_composio_actions_by_app(
|
|
225
|
+
composio_app_name: str,
|
|
226
|
+
server: SyncServer = Depends(get_letta_server),
|
|
227
|
+
):
|
|
228
|
+
"""
|
|
229
|
+
Get a list of all Composio actions for a specific app
|
|
230
|
+
"""
|
|
231
|
+
return server.get_composio_actions_from_app_name(composio_app_name=composio_app_name)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.post("/composio/{composio_action_name}", response_model=Tool, operation_id="add_composio_tool")
|
|
235
|
+
def add_composio_tool(
|
|
236
|
+
composio_action_name: str,
|
|
237
|
+
server: SyncServer = Depends(get_letta_server),
|
|
238
|
+
user_id: Optional[str] = Header(None, alias="user_id"),
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
|
|
242
|
+
"""
|
|
243
|
+
actor = server.get_user_or_default(user_id=user_id)
|
|
244
|
+
tool_create = ToolCreate.from_composio(action=composio_action_name)
|
|
245
|
+
return server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=actor)
|