letta-nightly 0.6.5.dev20241218213641__py3-none-any.whl → 0.6.5.dev20241220104040__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 +37 -6
- letta/client/client.py +2 -2
- letta/client/streaming.py +9 -9
- letta/errors.py +60 -25
- letta/functions/function_sets/base.py +0 -54
- letta/helpers/tool_rule_solver.py +82 -51
- letta/llm_api/llm_api_tools.py +2 -2
- letta/orm/custom_columns.py +5 -2
- letta/orm/message.py +2 -1
- letta/orm/passage.py +14 -15
- letta/providers.py +2 -1
- letta/schemas/enums.py +1 -0
- letta/schemas/letta_message.py +76 -40
- letta/schemas/letta_response.py +9 -1
- letta/schemas/message.py +13 -13
- letta/schemas/tool_rule.py +12 -2
- letta/server/rest_api/interface.py +48 -48
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +2 -2
- letta/server/rest_api/routers/v1/agents.py +3 -0
- letta/server/rest_api/routers/v1/tools.py +5 -20
- letta/server/rest_api/utils.py +23 -22
- letta/server/server.py +12 -18
- letta/services/agent_manager.py +32 -46
- letta/services/message_manager.py +1 -0
- letta/services/tool_manager.py +3 -3
- {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/RECORD +30 -30
- {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/entry_points.txt +0 -0
letta/orm/passage.py
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
|
-
from sqlalchemy import Column, JSON, Index
|
|
3
|
-
from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr
|
|
4
2
|
|
|
5
|
-
from
|
|
3
|
+
from sqlalchemy import JSON, Column, Index
|
|
4
|
+
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
|
|
5
|
+
|
|
6
|
+
from letta.config import LettaConfig
|
|
7
|
+
from letta.constants import MAX_EMBEDDING_DIM
|
|
6
8
|
from letta.orm.custom_columns import CommonVector, EmbeddingConfigColumn
|
|
7
|
-
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
8
9
|
from letta.orm.mixins import AgentMixin, FileMixin, OrganizationMixin, SourceMixin
|
|
10
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
9
11
|
from letta.schemas.passage import Passage as PydanticPassage
|
|
10
12
|
from letta.settings import settings
|
|
11
13
|
|
|
12
|
-
from letta.config import LettaConfig
|
|
13
|
-
from letta.constants import MAX_EMBEDDING_DIM
|
|
14
|
-
|
|
15
14
|
config = LettaConfig()
|
|
16
15
|
|
|
17
16
|
if TYPE_CHECKING:
|
|
18
|
-
from letta.orm.organization import Organization
|
|
19
17
|
from letta.orm.agent import Agent
|
|
18
|
+
from letta.orm.organization import Organization
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class BasePassage(SqlalchemyBase, OrganizationMixin):
|
|
23
22
|
"""Base class for all passage types with common fields"""
|
|
23
|
+
|
|
24
24
|
__abstract__ = True
|
|
25
25
|
__pydantic_model__ = PydanticPassage
|
|
26
26
|
|
|
@@ -45,17 +45,15 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
|
|
|
45
45
|
@declared_attr
|
|
46
46
|
def __table_args__(cls):
|
|
47
47
|
if settings.letta_pg_uri_no_default:
|
|
48
|
-
return (
|
|
49
|
-
Index(f'{cls.__tablename__}_org_idx', 'organization_id'),
|
|
50
|
-
{"extend_existing": True}
|
|
51
|
-
)
|
|
48
|
+
return (Index(f"{cls.__tablename__}_org_idx", "organization_id"), {"extend_existing": True})
|
|
52
49
|
return ({"extend_existing": True},)
|
|
53
50
|
|
|
54
51
|
|
|
55
52
|
class SourcePassage(BasePassage, FileMixin, SourceMixin):
|
|
56
53
|
"""Passages derived from external files/sources"""
|
|
54
|
+
|
|
57
55
|
__tablename__ = "source_passages"
|
|
58
|
-
|
|
56
|
+
|
|
59
57
|
@declared_attr
|
|
60
58
|
def file(cls) -> Mapped["FileMetadata"]:
|
|
61
59
|
"""Relationship to file"""
|
|
@@ -64,7 +62,7 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
|
|
|
64
62
|
@declared_attr
|
|
65
63
|
def organization(cls) -> Mapped["Organization"]:
|
|
66
64
|
return relationship("Organization", back_populates="source_passages", lazy="selectin")
|
|
67
|
-
|
|
65
|
+
|
|
68
66
|
@declared_attr
|
|
69
67
|
def source(cls) -> Mapped["Source"]:
|
|
70
68
|
"""Relationship to source"""
|
|
@@ -73,8 +71,9 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
|
|
|
73
71
|
|
|
74
72
|
class AgentPassage(BasePassage, AgentMixin):
|
|
75
73
|
"""Passages created by agents as archival memories"""
|
|
74
|
+
|
|
76
75
|
__tablename__ = "agent_passages"
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
@declared_attr
|
|
79
78
|
def organization(cls) -> Mapped["Organization"]:
|
|
80
79
|
return relationship("Organization", back_populates="agent_passages", lazy="selectin")
|
letta/providers.py
CHANGED
|
@@ -482,7 +482,8 @@ class GoogleAIProvider(Provider):
|
|
|
482
482
|
model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
|
|
483
483
|
|
|
484
484
|
# TODO remove manual filtering for gemini-pro
|
|
485
|
-
|
|
485
|
+
# Add support for all gemini models
|
|
486
|
+
model_options = [mo for mo in model_options if str(mo).startswith("gemini-")]
|
|
486
487
|
|
|
487
488
|
configs = []
|
|
488
489
|
for model in model_options:
|
letta/schemas/enums.py
CHANGED
|
@@ -45,5 +45,6 @@ class ToolRuleType(str, Enum):
|
|
|
45
45
|
run_first = "InitToolRule"
|
|
46
46
|
exit_loop = "TerminalToolRule" # reasoning loop should exit
|
|
47
47
|
continue_loop = "continue_loop" # reasoning loop should continue
|
|
48
|
+
conditional = "conditional"
|
|
48
49
|
constrain_child_tools = "ToolRule"
|
|
49
50
|
require_parent_tools = "require_parent_tools"
|
letta/schemas/letta_message.py
CHANGED
|
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, field_serializer, field_validator
|
|
|
9
9
|
|
|
10
10
|
class LettaMessage(BaseModel):
|
|
11
11
|
"""
|
|
12
|
-
Base class for simplified Letta message response type. This is intended to be used for developers who want the internal monologue,
|
|
12
|
+
Base class for simplified Letta message response type. This is intended to be used for developers who want the internal monologue, tool calls, and tool returns in a simplified format that does not include additional information other than the content and timestamp.
|
|
13
13
|
|
|
14
14
|
Attributes:
|
|
15
15
|
id (str): The ID of the message
|
|
@@ -60,32 +60,32 @@ class UserMessage(LettaMessage):
|
|
|
60
60
|
message: str
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
class
|
|
63
|
+
class ReasoningMessage(LettaMessage):
|
|
64
64
|
"""
|
|
65
|
-
Representation of an agent's internal
|
|
65
|
+
Representation of an agent's internal reasoning.
|
|
66
66
|
|
|
67
67
|
Attributes:
|
|
68
|
-
|
|
68
|
+
reasoning (str): The internal reasoning of the agent
|
|
69
69
|
id (str): The ID of the message
|
|
70
70
|
date (datetime): The date the message was created in ISO format
|
|
71
71
|
"""
|
|
72
72
|
|
|
73
|
-
message_type: Literal["
|
|
74
|
-
|
|
73
|
+
message_type: Literal["reasoning_message"] = "reasoning_message"
|
|
74
|
+
reasoning: str
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
class
|
|
77
|
+
class ToolCall(BaseModel):
|
|
78
78
|
|
|
79
79
|
name: str
|
|
80
80
|
arguments: str
|
|
81
|
-
|
|
81
|
+
tool_call_id: str
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
class
|
|
84
|
+
class ToolCallDelta(BaseModel):
|
|
85
85
|
|
|
86
86
|
name: Optional[str]
|
|
87
87
|
arguments: Optional[str]
|
|
88
|
-
|
|
88
|
+
tool_call_id: Optional[str]
|
|
89
89
|
|
|
90
90
|
# NOTE: this is a workaround to exclude None values from the JSON dump,
|
|
91
91
|
# since the OpenAI style of returning chunks doesn't include keys with null values
|
|
@@ -97,50 +97,84 @@ class FunctionCallDelta(BaseModel):
|
|
|
97
97
|
return json.dumps(self.model_dump(exclude_none=True), *args, **kwargs)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
class
|
|
100
|
+
class ToolCallMessage(LettaMessage):
|
|
101
101
|
"""
|
|
102
|
-
A message representing a request to call a
|
|
102
|
+
A message representing a request to call a tool (generated by the LLM to trigger tool execution).
|
|
103
103
|
|
|
104
104
|
Attributes:
|
|
105
|
-
|
|
105
|
+
tool_call (Union[ToolCall, ToolCallDelta]): The tool call
|
|
106
106
|
id (str): The ID of the message
|
|
107
107
|
date (datetime): The date the message was created in ISO format
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
|
-
message_type: Literal["
|
|
111
|
-
|
|
110
|
+
message_type: Literal["tool_call_message"] = "tool_call_message"
|
|
111
|
+
tool_call: Union[ToolCall, ToolCallDelta]
|
|
112
112
|
|
|
113
|
-
# NOTE: this is required for the
|
|
113
|
+
# NOTE: this is required for the ToolCallDelta exclude_none to work correctly
|
|
114
114
|
def model_dump(self, *args, **kwargs):
|
|
115
115
|
kwargs["exclude_none"] = True
|
|
116
116
|
data = super().model_dump(*args, **kwargs)
|
|
117
|
-
if isinstance(data["
|
|
118
|
-
data["
|
|
117
|
+
if isinstance(data["tool_call"], dict):
|
|
118
|
+
data["tool_call"] = {k: v for k, v in data["tool_call"].items() if v is not None}
|
|
119
119
|
return data
|
|
120
120
|
|
|
121
121
|
class Config:
|
|
122
122
|
json_encoders = {
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
ToolCallDelta: lambda v: v.model_dump(exclude_none=True),
|
|
124
|
+
ToolCall: lambda v: v.model_dump(exclude_none=True),
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
# NOTE: this is required to cast dicts into
|
|
127
|
+
# NOTE: this is required to cast dicts into ToolCallMessage objects
|
|
128
128
|
# Without this extra validator, Pydantic will throw an error if 'name' or 'arguments' are None
|
|
129
|
-
# (instead of properly casting to
|
|
130
|
-
@field_validator("
|
|
129
|
+
# (instead of properly casting to ToolCallDelta instead of ToolCall)
|
|
130
|
+
@field_validator("tool_call", mode="before")
|
|
131
131
|
@classmethod
|
|
132
|
-
def
|
|
132
|
+
def validate_tool_call(cls, v):
|
|
133
133
|
if isinstance(v, dict):
|
|
134
|
-
if "name" in v and "arguments" in v and "
|
|
135
|
-
return
|
|
136
|
-
elif "name" in v or "arguments" in v or "
|
|
137
|
-
return
|
|
134
|
+
if "name" in v and "arguments" in v and "tool_call_id" in v:
|
|
135
|
+
return ToolCall(name=v["name"], arguments=v["arguments"], tool_call_id=v["tool_call_id"])
|
|
136
|
+
elif "name" in v or "arguments" in v or "tool_call_id" in v:
|
|
137
|
+
return ToolCallDelta(name=v.get("name"), arguments=v.get("arguments"), tool_call_id=v.get("tool_call_id"))
|
|
138
138
|
else:
|
|
139
|
-
raise ValueError("
|
|
139
|
+
raise ValueError("tool_call must contain either 'name' or 'arguments'")
|
|
140
140
|
return v
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
class
|
|
143
|
+
class ToolReturnMessage(LettaMessage):
|
|
144
|
+
"""
|
|
145
|
+
A message representing the return value of a tool call (generated by Letta executing the requested tool).
|
|
146
|
+
|
|
147
|
+
Attributes:
|
|
148
|
+
tool_return (str): The return value of the tool
|
|
149
|
+
status (Literal["success", "error"]): The status of the tool call
|
|
150
|
+
id (str): The ID of the message
|
|
151
|
+
date (datetime): The date the message was created in ISO format
|
|
152
|
+
tool_call_id (str): A unique identifier for the tool call that generated this message
|
|
153
|
+
stdout (Optional[List(str)]): Captured stdout (e.g. prints, logs) from the tool invocation
|
|
154
|
+
stderr (Optional[List(str)]): Captured stderr from the tool invocation
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
message_type: Literal["tool_return_message"] = "tool_return_message"
|
|
158
|
+
tool_return: str
|
|
159
|
+
status: Literal["success", "error"]
|
|
160
|
+
tool_call_id: str
|
|
161
|
+
stdout: Optional[List[str]] = None
|
|
162
|
+
stderr: Optional[List[str]] = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Legacy Letta API had an additional type "assistant_message" and the "function_call" was a formatted string
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class AssistantMessage(LettaMessage):
|
|
169
|
+
message_type: Literal["assistant_message"] = "assistant_message"
|
|
170
|
+
assistant_message: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class LegacyFunctionCallMessage(LettaMessage):
|
|
174
|
+
function_call: str
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class LegacyFunctionReturn(LettaMessage):
|
|
144
178
|
"""
|
|
145
179
|
A message representing the return value of a function call (generated by Letta executing the requested function).
|
|
146
180
|
|
|
@@ -162,22 +196,24 @@ class FunctionReturn(LettaMessage):
|
|
|
162
196
|
stderr: Optional[List[str]] = None
|
|
163
197
|
|
|
164
198
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class AssistantMessage(LettaMessage):
|
|
169
|
-
message_type: Literal["assistant_message"] = "assistant_message"
|
|
170
|
-
assistant_message: str
|
|
199
|
+
class LegacyInternalMonologue(LettaMessage):
|
|
200
|
+
"""
|
|
201
|
+
Representation of an agent's internal monologue.
|
|
171
202
|
|
|
203
|
+
Attributes:
|
|
204
|
+
internal_monologue (str): The internal monologue of the agent
|
|
205
|
+
id (str): The ID of the message
|
|
206
|
+
date (datetime): The date the message was created in ISO format
|
|
207
|
+
"""
|
|
172
208
|
|
|
173
|
-
|
|
174
|
-
|
|
209
|
+
message_type: Literal["internal_monologue"] = "internal_monologue"
|
|
210
|
+
internal_monologue: str
|
|
175
211
|
|
|
176
212
|
|
|
177
|
-
LegacyLettaMessage = Union[
|
|
213
|
+
LegacyLettaMessage = Union[LegacyInternalMonologue, AssistantMessage, LegacyFunctionCallMessage, LegacyFunctionReturn]
|
|
178
214
|
|
|
179
215
|
|
|
180
216
|
LettaMessageUnion = Annotated[
|
|
181
|
-
Union[SystemMessage, UserMessage,
|
|
217
|
+
Union[SystemMessage, UserMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage, AssistantMessage],
|
|
182
218
|
Field(discriminator="message_type"),
|
|
183
219
|
]
|
letta/schemas/letta_response.py
CHANGED
|
@@ -40,14 +40,22 @@ class LettaResponse(BaseModel):
|
|
|
40
40
|
def get_formatted_content(msg):
|
|
41
41
|
if msg.message_type == "internal_monologue":
|
|
42
42
|
return f'<div class="content"><span class="internal-monologue">{html.escape(msg.internal_monologue)}</span></div>'
|
|
43
|
+
if msg.message_type == "reasoning_message":
|
|
44
|
+
return f'<div class="content"><span class="internal-monologue">{html.escape(msg.reasoning)}</span></div>'
|
|
43
45
|
elif msg.message_type == "function_call":
|
|
44
46
|
args = format_json(msg.function_call.arguments)
|
|
45
47
|
return f'<div class="content"><span class="function-name">{html.escape(msg.function_call.name)}</span>({args})</div>'
|
|
48
|
+
elif msg.message_type == "tool_call_message":
|
|
49
|
+
args = format_json(msg.tool_call.arguments)
|
|
50
|
+
return f'<div class="content"><span class="function-name">{html.escape(msg.function_call.name)}</span>({args})</div>'
|
|
46
51
|
elif msg.message_type == "function_return":
|
|
47
|
-
|
|
48
52
|
return_value = format_json(msg.function_return)
|
|
49
53
|
# return f'<div class="status-line">Status: {html.escape(msg.status)}</div><div class="content">{return_value}</div>'
|
|
50
54
|
return f'<div class="content">{return_value}</div>'
|
|
55
|
+
elif msg.message_type == "tool_return_message":
|
|
56
|
+
return_value = format_json(msg.tool_return)
|
|
57
|
+
# return f'<div class="status-line">Status: {html.escape(msg.status)}</div><div class="content">{return_value}</div>'
|
|
58
|
+
return f'<div class="content">{return_value}</div>'
|
|
51
59
|
elif msg.message_type == "user_message":
|
|
52
60
|
if is_json(msg.message):
|
|
53
61
|
return f'<div class="content">{format_json(msg.message)}</div>'
|
letta/schemas/message.py
CHANGED
|
@@ -16,10 +16,10 @@ from letta.schemas.enums import MessageRole
|
|
|
16
16
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
17
17
|
from letta.schemas.letta_message import (
|
|
18
18
|
AssistantMessage,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
ToolCall as LettaToolCall,
|
|
20
|
+
ToolCallMessage,
|
|
21
|
+
ToolReturnMessage,
|
|
22
|
+
ReasoningMessage,
|
|
23
23
|
LettaMessage,
|
|
24
24
|
SystemMessage,
|
|
25
25
|
UserMessage,
|
|
@@ -145,10 +145,10 @@ class Message(BaseMessage):
|
|
|
145
145
|
if self.text is not None:
|
|
146
146
|
# This is type InnerThoughts
|
|
147
147
|
messages.append(
|
|
148
|
-
|
|
148
|
+
ReasoningMessage(
|
|
149
149
|
id=self.id,
|
|
150
150
|
date=self.created_at,
|
|
151
|
-
|
|
151
|
+
reasoning=self.text,
|
|
152
152
|
)
|
|
153
153
|
)
|
|
154
154
|
if self.tool_calls is not None:
|
|
@@ -172,18 +172,18 @@ class Message(BaseMessage):
|
|
|
172
172
|
)
|
|
173
173
|
else:
|
|
174
174
|
messages.append(
|
|
175
|
-
|
|
175
|
+
ToolCallMessage(
|
|
176
176
|
id=self.id,
|
|
177
177
|
date=self.created_at,
|
|
178
|
-
|
|
178
|
+
tool_call=LettaToolCall(
|
|
179
179
|
name=tool_call.function.name,
|
|
180
180
|
arguments=tool_call.function.arguments,
|
|
181
|
-
|
|
181
|
+
tool_call_id=tool_call.id,
|
|
182
182
|
),
|
|
183
183
|
)
|
|
184
184
|
)
|
|
185
185
|
elif self.role == MessageRole.tool:
|
|
186
|
-
# This is type
|
|
186
|
+
# This is type ToolReturnMessage
|
|
187
187
|
# Try to interpret the function return, recall that this is how we packaged:
|
|
188
188
|
# def package_function_response(was_success, response_string, timestamp=None):
|
|
189
189
|
# formatted_time = get_local_time() if timestamp is None else timestamp
|
|
@@ -208,12 +208,12 @@ class Message(BaseMessage):
|
|
|
208
208
|
messages.append(
|
|
209
209
|
# TODO make sure this is what the API returns
|
|
210
210
|
# function_return may not match exactly...
|
|
211
|
-
|
|
211
|
+
ToolReturnMessage(
|
|
212
212
|
id=self.id,
|
|
213
213
|
date=self.created_at,
|
|
214
|
-
|
|
214
|
+
tool_return=self.text,
|
|
215
215
|
status=status_enum,
|
|
216
|
-
|
|
216
|
+
tool_call_id=self.tool_call_id,
|
|
217
217
|
)
|
|
218
218
|
)
|
|
219
219
|
elif self.role == MessageRole.user:
|
letta/schemas/tool_rule.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Union
|
|
1
|
+
from typing import Any, Dict, List, Optional, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
@@ -21,6 +21,16 @@ class ChildToolRule(BaseToolRule):
|
|
|
21
21
|
children: List[str] = Field(..., description="The children tools that can be invoked.")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class ConditionalToolRule(BaseToolRule):
|
|
25
|
+
"""
|
|
26
|
+
A ToolRule that conditionally maps to different child tools based on the output.
|
|
27
|
+
"""
|
|
28
|
+
type: ToolRuleType = ToolRuleType.conditional
|
|
29
|
+
default_child: Optional[str] = Field(None, description="The default child tool to be called. If None, any tool can be called.")
|
|
30
|
+
child_output_mapping: Dict[Any, str] = Field(..., description="The output case to check for mapping")
|
|
31
|
+
require_output_mapping: bool = Field(default=False, description="Whether to throw an error when output doesn't match any case")
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
class InitToolRule(BaseToolRule):
|
|
25
35
|
"""
|
|
26
36
|
Represents the initial tool rule configuration.
|
|
@@ -37,4 +47,4 @@ class TerminalToolRule(BaseToolRule):
|
|
|
37
47
|
type: ToolRuleType = ToolRuleType.exit_loop
|
|
38
48
|
|
|
39
49
|
|
|
40
|
-
ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule]
|
|
50
|
+
ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule]
|