letta-nightly 0.5.4.dev20241130104041__py3-none-any.whl → 0.5.4.dev20241201104110__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/agent.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  import inspect
3
+ import time
3
4
  import traceback
4
5
  import warnings
5
6
  from abc import ABC, abstractmethod
@@ -566,60 +567,60 @@ class Agent(BaseAgent):
566
567
  self,
567
568
  message_sequence: List[Message],
568
569
  function_call: str = "auto",
569
- first_message: bool = False, # hint
570
+ first_message: bool = False,
570
571
  stream: bool = False, # TODO move to config?
571
- fail_on_empty_response: bool = False,
572
572
  empty_response_retry_limit: int = 3,
573
+ backoff_factor: float = 0.5, # delay multiplier for exponential backoff
574
+ max_delay: float = 10.0, # max delay between retries
573
575
  ) -> ChatCompletionResponse:
574
- """Get response from LLM API"""
575
- # Get the allowed tools based on the ToolRulesSolver state
576
+ """Get response from LLM API with robust retry mechanism."""
577
+
576
578
  allowed_tool_names = self.tool_rules_solver.get_allowed_tool_names()
579
+ allowed_functions = (
580
+ self.functions if not allowed_tool_names else [func for func in self.functions if func["name"] in allowed_tool_names]
581
+ )
577
582
 
578
- if not allowed_tool_names:
579
- # if it's empty, any available tools are fair game
580
- allowed_functions = self.functions
581
- else:
582
- allowed_functions = [func for func in self.functions if func["name"] in allowed_tool_names]
583
+ for attempt in range(1, empty_response_retry_limit + 1):
584
+ try:
585
+ response = create(
586
+ llm_config=self.agent_state.llm_config,
587
+ messages=message_sequence,
588
+ user_id=self.agent_state.user_id,
589
+ functions=allowed_functions,
590
+ functions_python=self.functions_python,
591
+ function_call=function_call,
592
+ first_message=first_message,
593
+ stream=stream,
594
+ stream_interface=self.interface,
595
+ )
583
596
 
584
- try:
585
- response = create(
586
- # agent_state=self.agent_state,
587
- llm_config=self.agent_state.llm_config,
588
- messages=message_sequence,
589
- user_id=self.agent_state.user_id,
590
- functions=allowed_functions,
591
- functions_python=self.functions_python,
592
- function_call=function_call,
593
- # hint
594
- first_message=first_message,
595
- # streaming
596
- stream=stream,
597
- stream_interface=self.interface,
598
- )
597
+ # These bottom two are retryable
598
+ if len(response.choices) == 0 or response.choices[0] is None:
599
+ raise ValueError(f"API call returned an empty message: {response}")
599
600
 
600
- if len(response.choices) == 0 or response.choices[0] is None:
601
- empty_api_err_message = f"API call didn't return a message: {response}"
602
- if fail_on_empty_response or empty_response_retry_limit == 0:
603
- raise Exception(empty_api_err_message)
604
- else:
605
- # Decrement retry limit and try again
606
- warnings.warn(empty_api_err_message)
607
- return self._get_ai_reply(
608
- message_sequence, function_call, first_message, stream, fail_on_empty_response, empty_response_retry_limit - 1
609
- )
601
+ if response.choices[0].finish_reason not in ["stop", "function_call", "tool_calls"]:
602
+ if response.choices[0].finish_reason == "length":
603
+ # This is not retryable, hence RuntimeError v.s. ValueError
604
+ raise RuntimeError("Finish reason was length (maximum context length)")
605
+ else:
606
+ raise ValueError(f"Bad finish reason from API: {response.choices[0].finish_reason}")
607
+
608
+ return response
610
609
 
611
- # special case for 'length'
612
- if response.choices[0].finish_reason == "length":
613
- raise Exception("Finish reason was length (maximum context length)")
610
+ except ValueError as ve:
611
+ if attempt >= empty_response_retry_limit:
612
+ warnings.warn(f"Retry limit reached. Final error: {ve}")
613
+ break
614
+ else:
615
+ delay = min(backoff_factor * (2 ** (attempt - 1)), max_delay)
616
+ warnings.warn(f"Attempt {attempt} failed: {ve}. Retrying in {delay} seconds...")
617
+ time.sleep(delay)
614
618
 
615
- # catches for soft errors
616
- if response.choices[0].finish_reason not in ["stop", "function_call", "tool_calls"]:
617
- raise Exception(f"API call finish with bad finish reason: {response}")
619
+ except Exception as e:
620
+ # For non-retryable errors, exit immediately
621
+ raise e
618
622
 
619
- # unpack with response.choices[0].message.content
620
- return response
621
- except Exception as e:
622
- raise e
623
+ raise Exception("Retries exhausted and no valid response received.")
623
624
 
624
625
  def _handle_ai_response(
625
626
  self,
@@ -11,23 +11,54 @@ from letta.functions.schema_generator import generate_schema
11
11
 
12
12
 
13
13
  def derive_openai_json_schema(source_code: str, name: Optional[str] = None) -> dict:
14
- # auto-generate openai schema
14
+ """Derives the OpenAI JSON schema for a given function source code.
15
+
16
+ First, attempts to execute the source code in a custom environment with only the necessary imports.
17
+ Then, it generates the schema from the function's docstring and signature.
18
+ """
15
19
  try:
16
20
  # Define a custom environment with necessary imports
17
- env = {"Optional": Optional, "List": List, "Dict": Dict} # Add any other required imports here
18
-
21
+ env = {
22
+ "Optional": Optional,
23
+ "List": List,
24
+ "Dict": Dict,
25
+ # To support Pydantic models
26
+ # "BaseModel": BaseModel,
27
+ # "Field": Field,
28
+ }
19
29
  env.update(globals())
30
+
31
+ # print("About to execute source code...")
20
32
  exec(source_code, env)
33
+ # print("Source code executed successfully")
21
34
 
22
- # get available functions
23
- functions = [f for f in env if callable(env[f])]
35
+ functions = [f for f in env if callable(env[f]) and not f.startswith("__")]
36
+ if not functions:
37
+ raise LettaToolCreateError("No callable functions found in source code")
24
38
 
25
- # TODO: not sure if this always works
39
+ # print(f"Found functions: {functions}")
26
40
  func = env[functions[-1]]
27
- json_schema = generate_schema(func, name=name)
28
- return json_schema
41
+
42
+ if not hasattr(func, "__doc__") or not func.__doc__:
43
+ raise LettaToolCreateError(f"Function {func.__name__} missing docstring")
44
+
45
+ # print("About to generate schema...")
46
+ try:
47
+ schema = generate_schema(func, name=name)
48
+ # print("Schema generated successfully")
49
+ return schema
50
+ except TypeError as e:
51
+ raise LettaToolCreateError(f"Type error in schema generation: {str(e)}")
52
+ except ValueError as e:
53
+ raise LettaToolCreateError(f"Value error in schema generation: {str(e)}")
54
+ except Exception as e:
55
+ raise LettaToolCreateError(f"Unexpected error in schema generation: {str(e)}")
56
+
29
57
  except Exception as e:
30
- raise LettaToolCreateError(f"Failed to derive JSON schema from source code: {e}")
58
+ import traceback
59
+
60
+ traceback.print_exc()
61
+ raise LettaToolCreateError(f"Schema generation failed: {str(e)}") from e
31
62
 
32
63
 
33
64
  def parse_source_code(func) -> str:
@@ -22,7 +22,7 @@ def optional_length(annotation):
22
22
  raise ValueError("The annotation is not an Optional type")
23
23
 
24
24
 
25
- def type_to_json_schema_type(py_type):
25
+ def type_to_json_schema_type(py_type) -> dict:
26
26
  """
27
27
  Maps a Python type to a JSON schema type.
28
28
  Specifically handles typing.Optional and common Python types.
@@ -36,36 +36,87 @@ def type_to_json_schema_type(py_type):
36
36
  # Extract and map the inner type
37
37
  return type_to_json_schema_type(type_args[0])
38
38
 
39
+ # Handle Union types (except Optional which is handled above)
40
+ if get_origin(py_type) is Union:
41
+ # TODO support mapping Unions to anyOf
42
+ raise NotImplementedError("General Union types are not yet supported")
43
+
44
+ # Handle array types
45
+ origin = get_origin(py_type)
46
+ if py_type == list or origin in (list, List):
47
+ args = get_args(py_type)
48
+
49
+ if args and inspect.isclass(args[0]) and issubclass(args[0], BaseModel):
50
+ # If it's a list of Pydantic models, return an array with the model schema as items
51
+ return {
52
+ "type": "array",
53
+ "items": pydantic_model_to_json_schema(args[0]),
54
+ }
55
+
56
+ # Otherwise, recursively call the basic type checker
57
+ return {
58
+ "type": "array",
59
+ # get the type of the items in the list
60
+ "items": type_to_json_schema_type(args[0]),
61
+ }
62
+
63
+ # Handle object types
64
+ if py_type == dict or origin in (dict, Dict):
65
+ args = get_args(py_type)
66
+ if not args:
67
+ # Generic dict without type arguments
68
+ return {
69
+ "type": "object",
70
+ # "properties": {}
71
+ }
72
+ else:
73
+ raise ValueError(
74
+ f"Dictionary types {py_type} with nested type arguments are not supported (consider using a Pydantic model instead)"
75
+ )
76
+
77
+ # NOTE: the below code works for generic JSON schema parsing, but there's a problem with the key inference
78
+ # when it comes to OpenAI function schema generation so it doesn't make sense to allow for dict[str, Any] type hints
79
+ # key_type, value_type = args
80
+
81
+ # # Ensure dict keys are strings
82
+ # # Otherwise there's no JSON schema equivalent
83
+ # if key_type != str:
84
+ # raise ValueError("Dictionary keys must be strings for OpenAI function schema compatibility")
85
+
86
+ # # Handle value type to determine property schema
87
+ # value_schema = {}
88
+ # if inspect.isclass(value_type) and issubclass(value_type, BaseModel):
89
+ # value_schema = pydantic_model_to_json_schema(value_type)
90
+ # else:
91
+ # value_schema = type_to_json_schema_type(value_type)
92
+
93
+ # # NOTE: the problem lies here - the key is always "key_placeholder"
94
+ # return {"type": "object", "properties": {"key_placeholder": value_schema}}
95
+
96
+ # Handle direct Pydantic models
97
+ if inspect.isclass(py_type) and issubclass(py_type, BaseModel):
98
+ return pydantic_model_to_json_schema(py_type)
99
+
39
100
  # Mapping of Python types to JSON schema types
40
101
  type_map = {
41
102
  # Basic types
103
+ # Optional, Union, and collections are handled above ^
42
104
  int: "integer",
43
105
  str: "string",
44
106
  bool: "boolean",
45
107
  float: "number",
46
- # Collections
47
- List[str]: "array",
48
- List[int]: "array",
49
- list: "array",
50
- tuple: "array",
51
- set: "array",
52
- # Dictionaries
53
- dict: "object",
54
- Dict[str, Any]: "object",
55
- # Special types
56
108
  None: "null",
57
- type(None): "null",
58
- # Optional types
59
- # Optional[str]: "string", # NOTE: caught above ^
60
- Union[str, None]: "string",
61
109
  }
62
110
  if py_type not in type_map:
63
111
  raise ValueError(f"Python type {py_type} has no corresponding JSON schema type - full map: {type_map}")
64
-
65
- return type_map.get(py_type, "string") # Default to "string" if type not in map
112
+ else:
113
+ return {"type": type_map[py_type]}
66
114
 
67
115
 
68
- def pydantic_model_to_open_ai(model):
116
+ def pydantic_model_to_open_ai(model: Type[BaseModel]) -> dict:
117
+ """
118
+ Converts a Pydantic model as a singular arg to a JSON schema object for use in OpenAI function calling.
119
+ """
69
120
  schema = model.model_json_schema()
70
121
  docstring = parse(model.__doc__ or "")
71
122
  parameters = {k: v for k, v in schema.items() if k not in ("title", "description")}
@@ -80,7 +131,7 @@ def pydantic_model_to_open_ai(model):
80
131
  if docstring.short_description:
81
132
  schema["description"] = docstring.short_description
82
133
  else:
83
- raise
134
+ raise ValueError(f"No description found in docstring or description field (model: {model}, docstring: {docstring})")
84
135
 
85
136
  return {
86
137
  "name": schema["title"],
@@ -89,6 +140,159 @@ def pydantic_model_to_open_ai(model):
89
140
  }
90
141
 
91
142
 
143
+ def pydantic_model_to_json_schema(model: Type[BaseModel]) -> dict:
144
+ """
145
+ Converts a Pydantic model (as an arg that already is annotated) to a JSON schema object for use in OpenAI function calling.
146
+
147
+ An example of a Pydantic model as an arg:
148
+
149
+ class Step(BaseModel):
150
+ name: str = Field(
151
+ ...,
152
+ description="Name of the step.",
153
+ )
154
+ key: str = Field(
155
+ ...,
156
+ description="Unique identifier for the step.",
157
+ )
158
+ description: str = Field(
159
+ ...,
160
+ description="An exhaustic description of what this step is trying to achieve and accomplish.",
161
+ )
162
+
163
+ def create_task_plan(steps: list[Step]):
164
+ '''
165
+ Creates a task plan for the current task.
166
+
167
+ Args:
168
+ steps: List of steps to add to the task plan.
169
+ ...
170
+
171
+ Should result in:
172
+ {
173
+ "name": "create_task_plan",
174
+ "description": "Creates a task plan for the current task.",
175
+ "parameters": {
176
+ "type": "object",
177
+ "properties": {
178
+ "steps": { # <= this is the name of the arg
179
+ "type": "object",
180
+ "description": "List of steps to add to the task plan.",
181
+ "properties": {
182
+ "name": {
183
+ "type": "str",
184
+ "description": "Name of the step.",
185
+ },
186
+ "key": {
187
+ "type": "str",
188
+ "description": "Unique identifier for the step.",
189
+ },
190
+ "description": {
191
+ "type": "str",
192
+ "description": "An exhaustic description of what this step is trying to achieve and accomplish.",
193
+ },
194
+ },
195
+ "required": ["name", "key", "description"],
196
+ }
197
+ },
198
+ "required": ["steps"],
199
+ }
200
+ }
201
+
202
+ Specifically, the result of pydantic_model_to_json_schema(steps) (where `steps` is an instance of BaseModel) is:
203
+ {
204
+ "type": "object",
205
+ "properties": {
206
+ "name": {
207
+ "type": "str",
208
+ "description": "Name of the step."
209
+ },
210
+ "key": {
211
+ "type": "str",
212
+ "description": "Unique identifier for the step."
213
+ },
214
+ "description": {
215
+ "type": "str",
216
+ "description": "An exhaustic description of what this step is trying to achieve and accomplish."
217
+ },
218
+ },
219
+ "required": ["name", "key", "description"],
220
+ }
221
+ """
222
+ schema = model.model_json_schema()
223
+
224
+ def clean_property(prop: dict) -> dict:
225
+ """Clean up a property schema to match desired format"""
226
+
227
+ if "description" not in prop:
228
+ raise ValueError(f"Property {prop} lacks a 'description' key")
229
+
230
+ return {
231
+ "type": "string" if prop["type"] == "string" else prop["type"],
232
+ "description": prop["description"],
233
+ }
234
+
235
+ def resolve_ref(ref: str, schema: dict) -> dict:
236
+ """Resolve a $ref reference in the schema"""
237
+ if not ref.startswith("#/$defs/"):
238
+ raise ValueError(f"Unexpected reference format: {ref}")
239
+
240
+ model_name = ref.split("/")[-1]
241
+ if model_name not in schema.get("$defs", {}):
242
+ raise ValueError(f"Reference {model_name} not found in schema definitions")
243
+
244
+ return schema["$defs"][model_name]
245
+
246
+ def clean_schema(schema_part: dict, full_schema: dict) -> dict:
247
+ """Clean up a schema part, handling references and nested structures"""
248
+ # Handle $ref
249
+ if "$ref" in schema_part:
250
+ schema_part = resolve_ref(schema_part["$ref"], full_schema)
251
+
252
+ if "type" not in schema_part:
253
+ raise ValueError(f"Schema part lacks a 'type' key: {schema_part}")
254
+
255
+ # Handle array type
256
+ if schema_part["type"] == "array":
257
+ items_schema = schema_part["items"]
258
+ if "$ref" in items_schema:
259
+ items_schema = resolve_ref(items_schema["$ref"], full_schema)
260
+ return {"type": "array", "items": clean_schema(items_schema, full_schema), "description": schema_part.get("description", "")}
261
+
262
+ # Handle object type
263
+ if schema_part["type"] == "object":
264
+ if "properties" not in schema_part:
265
+ raise ValueError(f"Object schema lacks 'properties' key: {schema_part}")
266
+
267
+ properties = {}
268
+ for name, prop in schema_part["properties"].items():
269
+ if "items" in prop: # Handle arrays
270
+ if "description" not in prop:
271
+ raise ValueError(f"Property {prop} lacks a 'description' key")
272
+ properties[name] = {
273
+ "type": "array",
274
+ "items": clean_schema(prop["items"], full_schema),
275
+ "description": prop["description"],
276
+ }
277
+ else:
278
+ properties[name] = clean_property(prop)
279
+
280
+ pydantic_model_schema_dict = {
281
+ "type": "object",
282
+ "properties": properties,
283
+ "required": schema_part.get("required", []),
284
+ }
285
+ if "description" in schema_part:
286
+ pydantic_model_schema_dict["description"] = schema_part["description"]
287
+
288
+ return pydantic_model_schema_dict
289
+
290
+ # Handle primitive types
291
+ return clean_property(schema_part)
292
+
293
+ return clean_schema(schema_part=schema, full_schema=schema)
294
+
295
+
92
296
  def generate_schema(function, name: Optional[str] = None, description: Optional[str] = None) -> dict:
93
297
  # Get the signature of the function
94
298
  sig = inspect.signature(function)
@@ -126,24 +330,60 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
126
330
  if not param_doc or not param_doc.description:
127
331
  raise ValueError(f"Parameter '{param.name}' in function '{function.__name__}' lacks a description in the docstring")
128
332
 
129
- if inspect.isclass(param.annotation) and issubclass(param.annotation, BaseModel):
130
- schema["parameters"]["properties"][param.name] = pydantic_model_to_open_ai(param.annotation)
333
+ # If the parameter is a pydantic model, we need to unpack the Pydantic model type into a JSON schema object
334
+ # if inspect.isclass(param.annotation) and issubclass(param.annotation, BaseModel):
335
+ if (
336
+ (inspect.isclass(param.annotation) or inspect.isclass(get_origin(param.annotation) or param.annotation))
337
+ and not get_origin(param.annotation)
338
+ and issubclass(param.annotation, BaseModel)
339
+ ):
340
+ # print("Generating schema for pydantic model:", param.annotation)
341
+ # Extract the properties from the pydantic model
342
+ schema["parameters"]["properties"][param.name] = pydantic_model_to_json_schema(param.annotation)
343
+ schema["parameters"]["properties"][param.name]["description"] = param_doc.description
344
+
345
+ # Otherwise, we convert the Python typing to JSON schema types
346
+ # NOTE: important - if a dict or list, the internal type can be a Pydantic model itself
347
+ # however in that
131
348
  else:
132
- # Add parameter details to the schema
349
+ # print("Generating schema for non-pydantic model:", param.annotation)
350
+ # Grab the description for the parameter from the extended docstring
351
+ # If it doesn't exist, we should raise an error
133
352
  param_doc = next((d for d in docstring.params if d.arg_name == param.name), None)
134
- if param_doc:
135
- schema["parameters"]["properties"][param.name] = {
136
- # "type": "string" if param.annotation == str else str(param.annotation),
137
- "type": type_to_json_schema_type(param.annotation) if param.annotation != inspect.Parameter.empty else "string",
138
- "description": param_doc.description,
139
- }
140
- if param.default == inspect.Parameter.empty:
353
+ if not param_doc:
354
+ raise ValueError(f"Parameter '{param.name}' in function '{function.__name__}' lacks a description in the docstring")
355
+ elif not isinstance(param_doc.description, str):
356
+ raise ValueError(
357
+ f"Parameter '{param.name}' in function '{function.__name__}' has a description in the docstring that is not a string (type: {type(param_doc.description)})"
358
+ )
359
+ else:
360
+ # If it's a string or a basic type, then all you need is: (1) type, (2) description
361
+ # If it's a more complex type, then you also need either:
362
+ # - for array, you need "items", each of which has "type"
363
+ # - for a dict, you need "properties", which has keys which each have "type"
364
+ if param.annotation != inspect.Parameter.empty:
365
+ param_generated_schema = type_to_json_schema_type(param.annotation)
366
+ else:
367
+ # TODO why are we inferring here?
368
+ param_generated_schema = {"type": "string"}
369
+
370
+ # Add in the description
371
+ param_generated_schema["description"] = param_doc.description
372
+
373
+ # Add the schema to the function arg key
374
+ schema["parameters"]["properties"][param.name] = param_generated_schema
375
+
376
+ # If the parameter doesn't have a default value, it is required (so we need to add it to the required list)
377
+ if param.default == inspect.Parameter.empty and not is_optional(param.annotation):
141
378
  schema["parameters"]["required"].append(param.name)
142
379
 
380
+ # TODO what's going on here?
381
+ # If the parameter is a list of strings we need to hard cast to "string" instead of `str`
143
382
  if get_origin(param.annotation) is list:
144
383
  if get_args(param.annotation)[0] is str:
145
384
  schema["parameters"]["properties"][param.name]["items"] = {"type": "string"}
146
385
 
386
+ # TODO is this not duplicating the other append directly above?
147
387
  if param.annotation == inspect.Parameter.empty:
148
388
  schema["parameters"]["required"].append(param.name)
149
389
 
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 convert_to_structured_output(openai_function: dict) -> dict:
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": {"type": "object", "properties": {}, "additionalProperties": False, "required": []},
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
- structured_output["parameters"]["properties"][param] = {"type": details["type"], "description": details["description"]}
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
- # Add all properties to required list
35
- structured_output["parameters"]["required"] = list(structured_output["parameters"]["properties"].keys())
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
- tool["function"] = convert_to_structured_output(tool["function"])
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
- tool["function"] = convert_to_structured_output(tool["function"])
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
@@ -121,7 +121,7 @@ def num_tokens_from_functions(functions: List[dict], model: str = "gpt-4"):
121
121
  function_tokens += 3
122
122
  function_tokens += len(encoding.encode(o))
123
123
  else:
124
- print(f"Warning: not supported field {field}")
124
+ warnings.warn(f"num_tokens_from_functions: Unsupported field {field} in function {function}")
125
125
  function_tokens += 11
126
126
 
127
127
  num_tokens += function_tokens
@@ -134,7 +134,7 @@ def create_application() -> "FastAPI":
134
134
 
135
135
  if "--ade" in sys.argv:
136
136
  settings.cors_origins.append("https://app.letta.com")
137
- print(f"▶ View using ADE at: https://app.letta.com/local-project/agents")
137
+ print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard")
138
138
 
139
139
  if "--secure" in sys.argv:
140
140
  print(f"▶ Using secure mode with password: {random_password}")
@@ -276,6 +276,25 @@ class ToolExecutionSandbox:
276
276
 
277
277
  return code
278
278
 
279
+ def _convert_param_to_value(self, param_type: str, raw_value: str) -> str:
280
+
281
+ if param_type == "string":
282
+ value = '"' + raw_value + '"'
283
+
284
+ elif param_type == "integer" or param_type == "boolean" or param_type == "number":
285
+ value = raw_value
286
+
287
+ elif param_type == "array":
288
+ value = raw_value
289
+
290
+ elif param_type == "object":
291
+ value = raw_value
292
+
293
+ else:
294
+ raise TypeError(f"Unsupported type: {param_type}, raw_value={raw_value}")
295
+
296
+ return str(value)
297
+
279
298
  def initialize_param(self, name: str, raw_value: str) -> str:
280
299
  params = self.tool.json_schema["parameters"]["properties"]
281
300
  spec = params.get(name)
@@ -287,14 +306,9 @@ class ToolExecutionSandbox:
287
306
  if param_type is None and spec.get("parameters"):
288
307
  param_type = spec["parameters"].get("type")
289
308
 
290
- if param_type == "string":
291
- value = '"' + raw_value + '"'
292
- elif param_type == "integer" or param_type == "boolean":
293
- value = raw_value
294
- else:
295
- raise TypeError(f"unsupported type: {param_type}")
309
+ value = self._convert_param_to_value(param_type, raw_value)
296
310
 
297
- return name + " = " + str(value) + "\n"
311
+ return name + " = " + value + "\n"
298
312
 
299
313
  def invoke_function_call(self, inject_agent_state: bool) -> str:
300
314
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.5.4.dev20241130104041
3
+ Version: 0.5.4.dev20241201104110
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -1,6 +1,6 @@
1
1
  letta/__init__.py,sha256=FGDVS-TIQ5GQN2d3DaYbDaEaDCH54TpwzX5lSn60KwY,1035
2
2
  letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
- letta/agent.py,sha256=-4L2daggVPMjZylLFbilfcpzmjfrSQXHFpqbRmqPQRs,77352
3
+ letta/agent.py,sha256=9M0emIYjOlE3ZlJJYReDYZO84Atnd1ds7Je5KOEYcjE,77519
4
4
  letta/agent_store/chroma.py,sha256=-kCEMBFKmqCyFeIETYf7RN-khGddsip2FAhSzNqaC7U,12537
5
5
  letta/agent_store/db.py,sha256=n15t8qhHfqhtFDxSQg_9uwvMntpWml8Jz_Y-ofL0loQ,23467
6
6
  letta/agent_store/lancedb.py,sha256=i63d4VZwj9UIOTNs5f0JZ_r5yZD-jKWz4FAH4RMpXOE,5104
@@ -26,9 +26,9 @@ letta/errors.py,sha256=mFeTpZP37otDMr68s9hyGOnafJPrWeblQOI79cgP4nQ,3209
26
26
  letta/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  letta/functions/function_sets/base.py,sha256=9Rs8SNrtUgqYtlmztE1gVO6FEn864u8t-X1qik24nps,8096
28
28
  letta/functions/function_sets/extras.py,sha256=Jik3UiDqYTm4Lam1XPTvuVjvgUHwIAhopsnbmVhGMBg,4732
29
- letta/functions/functions.py,sha256=7FankCCKpUgBUnrzmF96sRy8fHyfkfYFFJ_f45MsZh0,4196
29
+ letta/functions/functions.py,sha256=qCoU9w51uXC8NODHr6dj_tz69NB4924I9PToN2yh2NA,5418
30
30
  letta/functions/helpers.py,sha256=K84kqAN1RXZIhjb7-btS0C2p-SInYNv6FvSfo-16Y6g,8578
31
- letta/functions/schema_generator.py,sha256=PIuey3ZsMvOTBViPWlqajleu48LOEEkcxnLhRPkyRsE,9076
31
+ letta/functions/schema_generator.py,sha256=Y0rQjJBI8Z5fSKmT71EGXtHpIvNb3dMM5X00TP89tlY,19330
32
32
  letta/helpers/__init__.py,sha256=p0luQ1Oe3Skc6sH4O58aHHA3Qbkyjifpuq0DZ1GAY0U,59
33
33
  letta/helpers/tool_rule_solver.py,sha256=YCwawbRUQw10ZVR17WYXo8b5roxdGe-B5nNVMqlAgBE,4826
34
34
  letta/humans/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,10 +41,10 @@ letta/llm_api/azure_openai.py,sha256=Y1HKPog1XzM_f7ujUK_Gv2zQkoy5pU-1bKiUnvSxSrs
41
41
  letta/llm_api/azure_openai_constants.py,sha256=oXtKrgBFHf744gyt5l1thILXgyi8NDNUrKEa2GGGpjw,278
42
42
  letta/llm_api/cohere.py,sha256=vDRd-SUGp1t_JUIdwC3RkIhwMl0OY7n-tAU9uPORYkY,14826
43
43
  letta/llm_api/google_ai.py,sha256=xKz9JDZs3m6yzSfcgCAAUD_rjI20BBIINoiSvlcnOw0,17621
44
- letta/llm_api/helpers.py,sha256=KqkdjZWYghx4OPwLcHEC6ruc_z9DScbysw3VH4x9A0Q,9887
44
+ letta/llm_api/helpers.py,sha256=F8xZDZgDojWX5v-0vakyeUQyCyBr1HmzmsITRdOsmVg,13457
45
45
  letta/llm_api/llm_api_tools.py,sha256=h2eudFygI6yFIOaA5Q9GmhiwMPq2mHQyhoSHbn57CCE,16866
46
46
  letta/llm_api/mistral.py,sha256=fHdfD9ug-rQIk2qn8tRKay1U6w9maF11ryhKi91FfXM,1593
47
- letta/llm_api/openai.py,sha256=gGuxfE4_TURwnKfleogDCURvKe4UFb7KitoCWqWqKkI,23844
47
+ letta/llm_api/openai.py,sha256=Z3xNoJPtplzNU5Lj8JkQg8lJkSb18QKIpFTfLRoaK5E,24180
48
48
  letta/local_llm/README.md,sha256=hFJyw5B0TU2jrh9nb0zGZMgdH-Ei1dSRfhvPQG_NSoU,168
49
49
  letta/local_llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  letta/local_llm/chat_completion_proxy.py,sha256=SiohxsjGTku4vOryOZx7I0t0xoO_sUuhXgoe62fKq3c,12995
@@ -76,7 +76,7 @@ letta/local_llm/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
76
76
  letta/local_llm/settings/deterministic_mirostat.py,sha256=kgRikcxYHfIbPFydHW6W7IO9jmp6NeA7JNAhnI3DPsc,1221
77
77
  letta/local_llm/settings/settings.py,sha256=ZAbzDpu2WsBXjVGXJ-TKUpS99VTI__3EoZml9KqYef0,2971
78
78
  letta/local_llm/settings/simple.py,sha256=HAO2jBJ_hJCEsXWIJcD0sckR0tI0zs3x2CPdf6ORQLs,719
79
- letta/local_llm/utils.py,sha256=0DELikgq82aztlN-sAOSuDLWbG0IVD67P7kHHjJ6nF0,13101
79
+ letta/local_llm/utils.py,sha256=4nS5I2PpUm20QK4E-fgamEUaDXJsvxpCPIOCr-PVB0M,13148
80
80
  letta/local_llm/vllm/api.py,sha256=2kAGZjc_GH9ILJnVRq-45yfsfKELVfbC9VEl_cIC6vg,2590
81
81
  letta/local_llm/webui/api.py,sha256=kkxncdCFq1vjgvaHOoQ__j7rcDPgC1F64KcEm94Y6Rs,2639
82
82
  letta/local_llm/webui/legacy_api.py,sha256=k3H3y4qp2Fs-XmP24iSIEyvq6wjWFWBzklY3-wRAJNI,2335
@@ -164,7 +164,7 @@ letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
164
164
  letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
165
165
  letta/server/generate_openapi_schema.sh,sha256=0OtBhkC1g6CobVmNEd_m2B6sTdppjbJLXaM95icejvE,371
166
166
  letta/server/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
- letta/server/rest_api/app.py,sha256=QbCgYeyzA86iefwomROBAaxwvSWYQT10fhHcNZsyjH4,7555
167
+ letta/server/rest_api/app.py,sha256=U5plRL_-doYlHSv071DeUl3GNA1UsLWsVaEfGrLKZUU,7570
168
168
  letta/server/rest_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
169
169
  letta/server/rest_api/auth/index.py,sha256=fQBGyVylGSRfEMLQ17cZzrHd5Y1xiVylvPqH5Rl-lXQ,1378
170
170
  letta/server/rest_api/auth_token.py,sha256=725EFEIiNj4dh70hrSd94UysmFD8vcJLrTRfNHkzxDo,774
@@ -211,7 +211,7 @@ letta/services/organization_manager.py,sha256=OfE2_NMmhqXURX4sg7hCOiFQVQpV5ZiPu7
211
211
  letta/services/per_agent_lock_manager.py,sha256=02iw5e-xoLiKGqqn2KdJdk-QlrDHPz5oMuEs1ibwXHA,540
212
212
  letta/services/sandbox_config_manager.py,sha256=9BCu59nHR4nIMFXgFyEMOY2UTmZvBMS3GlDBWWCHB4I,12648
213
213
  letta/services/source_manager.py,sha256=StX5Wfd7XSCKJet8qExIu3GMoI-eMIbEarAeTv2gq0s,6555
214
- letta/services/tool_execution_sandbox.py,sha256=VHsdRYYWkp5UQOzqmznsIOIXn7iUiXRGxNqv655YXwU,13100
214
+ letta/services/tool_execution_sandbox.py,sha256=y14mlb8r3mppAb5l977t0zy2ywCCjOboFuqSvT41X1M,13457
215
215
  letta/services/tool_manager.py,sha256=FVCB9R3NFahh-KE5jROzf6J9WEgqhqGoDk5RpWjlgjg,7835
216
216
  letta/services/tool_sandbox_env/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
217
  letta/services/user_manager.py,sha256=UJa0hqCjz0yXtvrCR8OVBqlSR5lC_Ejn-uG__58zLds,4398
@@ -220,8 +220,8 @@ letta/streaming_interface.py,sha256=_FPUWy58j50evHcpXyd7zB1wWqeCc71NCFeWh_TBvnw,
220
220
  letta/streaming_utils.py,sha256=329fsvj1ZN0r0LpQtmMPZ2vSxkDBIUUwvGHZFkjm2I8,11745
221
221
  letta/system.py,sha256=buKYPqG5n2x41hVmWpu6JUpyd7vTWED9Km2_M7dLrvk,6960
222
222
  letta/utils.py,sha256=COwQLAt02eEM9tjp6p5kN8YeTqGXr714l5BvffLVCLU,32376
223
- letta_nightly-0.5.4.dev20241130104041.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
224
- letta_nightly-0.5.4.dev20241130104041.dist-info/METADATA,sha256=6eKk3xGkeaLXojj7YcGYym-Z1FiX9G34GeZmFgT4wf8,11515
225
- letta_nightly-0.5.4.dev20241130104041.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
226
- letta_nightly-0.5.4.dev20241130104041.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
227
- letta_nightly-0.5.4.dev20241130104041.dist-info/RECORD,,
223
+ letta_nightly-0.5.4.dev20241201104110.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
224
+ letta_nightly-0.5.4.dev20241201104110.dist-info/METADATA,sha256=5QfUql5hNMn_8aOPuyG3cVU4Gm8JcVX8GFdrM1DBPkI,11515
225
+ letta_nightly-0.5.4.dev20241201104110.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
226
+ letta_nightly-0.5.4.dev20241201104110.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
227
+ letta_nightly-0.5.4.dev20241201104110.dist-info/RECORD,,