letta-nightly 0.6.4.dev20241213193437__py3-none-any.whl → 0.6.4.dev20241214104034__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 (62) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +54 -45
  3. letta/chat_only_agent.py +6 -8
  4. letta/cli/cli.py +2 -10
  5. letta/client/client.py +121 -138
  6. letta/config.py +0 -161
  7. letta/main.py +3 -8
  8. letta/memory.py +3 -14
  9. letta/o1_agent.py +1 -5
  10. letta/offline_memory_agent.py +2 -6
  11. letta/orm/__init__.py +2 -0
  12. letta/orm/agent.py +109 -0
  13. letta/orm/agents_tags.py +10 -18
  14. letta/orm/block.py +29 -4
  15. letta/orm/blocks_agents.py +5 -11
  16. letta/orm/custom_columns.py +152 -0
  17. letta/orm/message.py +3 -38
  18. letta/orm/organization.py +2 -7
  19. letta/orm/passage.py +10 -32
  20. letta/orm/source.py +5 -25
  21. letta/orm/sources_agents.py +13 -0
  22. letta/orm/sqlalchemy_base.py +54 -30
  23. letta/orm/tool.py +1 -19
  24. letta/orm/tools_agents.py +7 -24
  25. letta/orm/user.py +3 -4
  26. letta/schemas/agent.py +48 -65
  27. letta/schemas/memory.py +2 -1
  28. letta/schemas/sandbox_config.py +12 -1
  29. letta/server/rest_api/app.py +0 -5
  30. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
  31. letta/server/rest_api/routers/v1/agents.py +99 -78
  32. letta/server/rest_api/routers/v1/blocks.py +22 -25
  33. letta/server/rest_api/routers/v1/jobs.py +4 -4
  34. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -10
  35. letta/server/rest_api/routers/v1/sources.py +12 -12
  36. letta/server/rest_api/routers/v1/tools.py +35 -15
  37. letta/server/rest_api/routers/v1/users.py +0 -46
  38. letta/server/server.py +172 -716
  39. letta/server/ws_api/server.py +0 -5
  40. letta/services/agent_manager.py +405 -0
  41. letta/services/block_manager.py +13 -21
  42. letta/services/helpers/agent_manager_helper.py +90 -0
  43. letta/services/organization_manager.py +0 -1
  44. letta/services/passage_manager.py +62 -62
  45. letta/services/sandbox_config_manager.py +3 -3
  46. letta/services/source_manager.py +22 -1
  47. letta/services/user_manager.py +11 -6
  48. letta/utils.py +2 -2
  49. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/METADATA +1 -1
  50. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/RECORD +53 -57
  51. letta/metadata.py +0 -407
  52. letta/schemas/agents_tags.py +0 -33
  53. letta/schemas/api_key.py +0 -21
  54. letta/schemas/blocks_agents.py +0 -32
  55. letta/schemas/tools_agents.py +0 -32
  56. letta/server/rest_api/routers/openai/assistants/threads.py +0 -338
  57. letta/services/agents_tags_manager.py +0 -64
  58. letta/services/blocks_agents_manager.py +0 -106
  59. letta/services/tools_agents_manager.py +0 -94
  60. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/LICENSE +0 -0
  61. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/WHEEL +0 -0
  62. {letta_nightly-0.6.4.dev20241213193437.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/entry_points.txt +0 -0
letta/metadata.py DELETED
@@ -1,407 +0,0 @@
1
- """ Metadata store for user/agent/data_source information"""
2
-
3
- import os
4
- import secrets
5
- from typing import List, Optional, Union
6
-
7
- from sqlalchemy import JSON, Column, DateTime, Index, String, TypeDecorator
8
- from sqlalchemy.sql import func
9
-
10
- from letta.config import LettaConfig
11
- from letta.orm.base import Base
12
- from letta.schemas.agent import PersistedAgentState
13
- from letta.schemas.api_key import APIKey
14
- from letta.schemas.embedding_config import EmbeddingConfig
15
- from letta.schemas.enums import ToolRuleType
16
- from letta.schemas.llm_config import LLMConfig
17
- from letta.schemas.tool_rule import ChildToolRule, InitToolRule, TerminalToolRule
18
- from letta.schemas.user import User
19
- from letta.services.per_agent_lock_manager import PerAgentLockManager
20
- from letta.settings import settings
21
- from letta.utils import enforce_types, printd
22
-
23
-
24
- class LLMConfigColumn(TypeDecorator):
25
- """Custom type for storing LLMConfig as JSON"""
26
-
27
- impl = JSON
28
- cache_ok = True
29
-
30
- def load_dialect_impl(self, dialect):
31
- return dialect.type_descriptor(JSON())
32
-
33
- def process_bind_param(self, value, dialect):
34
- if value:
35
- # return vars(value)
36
- if isinstance(value, LLMConfig):
37
- return value.model_dump()
38
- return value
39
-
40
- def process_result_value(self, value, dialect):
41
- if value:
42
- return LLMConfig(**value)
43
- return value
44
-
45
-
46
- class EmbeddingConfigColumn(TypeDecorator):
47
- """Custom type for storing EmbeddingConfig as JSON"""
48
-
49
- impl = JSON
50
- cache_ok = True
51
-
52
- def load_dialect_impl(self, dialect):
53
- return dialect.type_descriptor(JSON())
54
-
55
- def process_bind_param(self, value, dialect):
56
- if value:
57
- # return vars(value)
58
- if isinstance(value, EmbeddingConfig):
59
- return value.model_dump()
60
- return value
61
-
62
- def process_result_value(self, value, dialect):
63
- if value:
64
- return EmbeddingConfig(**value)
65
- return value
66
-
67
-
68
- # TODO: eventually store providers?
69
- # class Provider(Base):
70
- # __tablename__ = "providers"
71
- # __table_args__ = {"extend_existing": True}
72
- #
73
- # id = Column(String, primary_key=True)
74
- # name = Column(String, nullable=False)
75
- # created_at = Column(DateTime(timezone=True))
76
- # api_key = Column(String, nullable=False)
77
- # base_url = Column(String, nullable=False)
78
-
79
-
80
- class APIKeyModel(Base):
81
- """Data model for authentication tokens. One-to-many relationship with UserModel (1 User - N tokens)."""
82
-
83
- __tablename__ = "tokens"
84
-
85
- id = Column(String, primary_key=True)
86
- # each api key is tied to a user account (that it validates access for)
87
- user_id = Column(String, nullable=False)
88
- # the api key
89
- key = Column(String, nullable=False)
90
- # extra (optional) metadata
91
- name = Column(String)
92
-
93
- Index(__tablename__ + "_idx_user", user_id),
94
- Index(__tablename__ + "_idx_key", key),
95
-
96
- def __repr__(self) -> str:
97
- return f"<APIKey(id='{self.id}', key='{self.key}', name='{self.name}')>"
98
-
99
- def to_record(self) -> User:
100
- return APIKey(
101
- id=self.id,
102
- user_id=self.user_id,
103
- key=self.key,
104
- name=self.name,
105
- )
106
-
107
-
108
- def generate_api_key(prefix="sk-", length=51) -> str:
109
- # Generate 'length // 2' bytes because each byte becomes two hex digits. Adjust length for prefix.
110
- actual_length = max(length - len(prefix), 1) // 2 # Ensure at least 1 byte is generated
111
- random_bytes = secrets.token_bytes(actual_length)
112
- new_key = prefix + random_bytes.hex()
113
- return new_key
114
-
115
-
116
- class ToolRulesColumn(TypeDecorator):
117
- """Custom type for storing a list of ToolRules as JSON"""
118
-
119
- impl = JSON
120
- cache_ok = True
121
-
122
- def load_dialect_impl(self, dialect):
123
- return dialect.type_descriptor(JSON())
124
-
125
- def process_bind_param(self, value, dialect):
126
- """Convert a list of ToolRules to JSON-serializable format."""
127
- if value:
128
- data = [rule.model_dump() for rule in value]
129
- for d in data:
130
- d["type"] = d["type"].value
131
-
132
- for d in data:
133
- assert not (d["type"] == "ToolRule" and "children" not in d), "ToolRule does not have children field"
134
- return data
135
- return value
136
-
137
- def process_result_value(self, value, dialect) -> List[Union[ChildToolRule, InitToolRule, TerminalToolRule]]:
138
- """Convert JSON back to a list of ToolRules."""
139
- if value:
140
- return [self.deserialize_tool_rule(rule_data) for rule_data in value]
141
- return value
142
-
143
- @staticmethod
144
- def deserialize_tool_rule(data: dict) -> Union[ChildToolRule, InitToolRule, TerminalToolRule]:
145
- """Deserialize a dictionary to the appropriate ToolRule subclass based on the 'type'."""
146
- rule_type = ToolRuleType(data.get("type")) # Remove 'type' field if it exists since it is a class var
147
- if rule_type == ToolRuleType.run_first:
148
- return InitToolRule(**data)
149
- elif rule_type == ToolRuleType.exit_loop:
150
- return TerminalToolRule(**data)
151
- elif rule_type == ToolRuleType.constrain_child_tools:
152
- rule = ChildToolRule(**data)
153
- return rule
154
- else:
155
- raise ValueError(f"Unknown tool rule type: {rule_type}")
156
-
157
-
158
- class AgentModel(Base):
159
- """Defines data model for storing Passages (consisting of text, embedding)"""
160
-
161
- __tablename__ = "agents"
162
- __table_args__ = {"extend_existing": True}
163
-
164
- id = Column(String, primary_key=True)
165
- user_id = Column(String, nullable=False)
166
- name = Column(String, nullable=False)
167
- created_at = Column(DateTime(timezone=True), server_default=func.now())
168
- description = Column(String)
169
-
170
- # state (context compilation)
171
- message_ids = Column(JSON)
172
- system = Column(String)
173
-
174
- # configs
175
- agent_type = Column(String)
176
- llm_config = Column(LLMConfigColumn)
177
- embedding_config = Column(EmbeddingConfigColumn)
178
-
179
- # state
180
- metadata_ = Column(JSON)
181
-
182
- # tools
183
- tool_names = Column(JSON)
184
- tool_rules = Column(ToolRulesColumn)
185
-
186
- Index(__tablename__ + "_idx_user", user_id),
187
-
188
- def __repr__(self) -> str:
189
- return f"<Agent(id='{self.id}', name='{self.name}')>"
190
-
191
- def to_record(self) -> PersistedAgentState:
192
- agent_state = PersistedAgentState(
193
- id=self.id,
194
- user_id=self.user_id,
195
- name=self.name,
196
- created_at=self.created_at,
197
- description=self.description,
198
- message_ids=self.message_ids,
199
- system=self.system,
200
- tool_names=self.tool_names,
201
- tool_rules=self.tool_rules,
202
- agent_type=self.agent_type,
203
- llm_config=self.llm_config,
204
- embedding_config=self.embedding_config,
205
- metadata_=self.metadata_,
206
- )
207
- return agent_state
208
-
209
-
210
- class AgentSourceMappingModel(Base):
211
- """Stores mapping between agent -> source"""
212
-
213
- __tablename__ = "agent_source_mapping"
214
-
215
- id = Column(String, primary_key=True)
216
- user_id = Column(String, nullable=False)
217
- agent_id = Column(String, nullable=False)
218
- source_id = Column(String, nullable=False)
219
- Index(__tablename__ + "_idx_user", user_id, agent_id, source_id),
220
-
221
- def __repr__(self) -> str:
222
- return f"<AgentSourceMapping(user_id='{self.user_id}', agent_id='{self.agent_id}', source_id='{self.source_id}')>"
223
-
224
-
225
- class MetadataStore:
226
- uri: Optional[str] = None
227
-
228
- def __init__(self, config: LettaConfig):
229
- # TODO: get DB URI or path
230
- if config.metadata_storage_type == "postgres":
231
- # construct URI from enviornment variables
232
- self.uri = settings.pg_uri if settings.pg_uri else config.metadata_storage_uri
233
-
234
- elif config.metadata_storage_type == "sqlite":
235
- path = os.path.join(config.metadata_storage_path, "sqlite.db")
236
- self.uri = f"sqlite:///{path}"
237
- else:
238
- raise ValueError(f"Invalid metadata storage type: {config.metadata_storage_type}")
239
-
240
- # Ensure valid URI
241
- assert self.uri, "Database URI is not provided or is invalid."
242
-
243
- from letta.server.server import db_context
244
-
245
- self.session_maker = db_context
246
-
247
- @enforce_types
248
- def create_api_key(self, user_id: str, name: str) -> APIKey:
249
- """Create an API key for a user"""
250
- new_api_key = generate_api_key()
251
- with self.session_maker() as session:
252
- if session.query(APIKeyModel).filter(APIKeyModel.key == new_api_key).count() > 0:
253
- # NOTE duplicate API keys / tokens should never happen, but if it does don't allow it
254
- raise ValueError(f"Token {new_api_key} already exists")
255
- # TODO store the API keys as hashed
256
- assert user_id and name, "User ID and name must be provided"
257
- token = APIKey(user_id=user_id, key=new_api_key, name=name)
258
- session.add(APIKeyModel(**vars(token)))
259
- session.commit()
260
- return self.get_api_key(api_key=new_api_key)
261
-
262
- @enforce_types
263
- def delete_api_key(self, api_key: str):
264
- """Delete an API key from the database"""
265
- with self.session_maker() as session:
266
- session.query(APIKeyModel).filter(APIKeyModel.key == api_key).delete()
267
- session.commit()
268
-
269
- @enforce_types
270
- def get_api_key(self, api_key: str) -> Optional[APIKey]:
271
- with self.session_maker() as session:
272
- results = session.query(APIKeyModel).filter(APIKeyModel.key == api_key).all()
273
- if len(results) == 0:
274
- return None
275
- assert len(results) == 1, f"Expected 1 result, got {len(results)}" # should only be one result
276
- return results[0].to_record()
277
-
278
- @enforce_types
279
- def get_all_api_keys_for_user(self, user_id: str) -> List[APIKey]:
280
- with self.session_maker() as session:
281
- results = session.query(APIKeyModel).filter(APIKeyModel.user_id == user_id).all()
282
- tokens = [r.to_record() for r in results]
283
- return tokens
284
-
285
- @enforce_types
286
- def create_agent(self, agent: PersistedAgentState):
287
- # insert into agent table
288
- # make sure agent.name does not already exist for user user_id
289
- with self.session_maker() as session:
290
- if session.query(AgentModel).filter(AgentModel.name == agent.name).filter(AgentModel.user_id == agent.user_id).count() > 0:
291
- raise ValueError(f"Agent with name {agent.name} already exists")
292
- fields = vars(agent)
293
- # fields["memory"] = agent.memory.to_dict()
294
- # if "_internal_memory" in fields:
295
- # del fields["_internal_memory"]
296
- # else:
297
- # warnings.warn(f"Agent {agent.id} has no _internal_memory field")
298
- if "tags" in fields:
299
- del fields["tags"]
300
- # else:
301
- # warnings.warn(f"Agent {agent.id} has no tags field")
302
- session.add(AgentModel(**fields))
303
- session.commit()
304
-
305
- @enforce_types
306
- def update_agent(self, agent: PersistedAgentState):
307
- with self.session_maker() as session:
308
- fields = vars(agent)
309
- # if isinstance(agent.memory, Memory): # TODO: this is nasty but this whole class will soon be removed so whatever
310
- # fields["memory"] = agent.memory.to_dict()
311
- # if "_internal_memory" in fields:
312
- # del fields["_internal_memory"]
313
- # else:
314
- # warnings.warn(f"Agent {agent.id} has no _internal_memory field")
315
- if "tags" in fields:
316
- del fields["tags"]
317
- # else:
318
- # warnings.warn(f"Agent {agent.id} has no tags field")
319
- session.query(AgentModel).filter(AgentModel.id == agent.id).update(fields)
320
- session.commit()
321
-
322
- @enforce_types
323
- def delete_agent(self, agent_id: str, per_agent_lock_manager: PerAgentLockManager):
324
- # TODO: Remove this once Agent is on the ORM
325
- # TODO: To prevent unbounded growth
326
- per_agent_lock_manager.clear_lock(agent_id)
327
-
328
- with self.session_maker() as session:
329
-
330
- # delete agents
331
- session.query(AgentModel).filter(AgentModel.id == agent_id).delete()
332
-
333
- # delete mappings
334
- session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.agent_id == agent_id).delete()
335
-
336
- session.commit()
337
-
338
- @enforce_types
339
- def list_agents(self, user_id: str) -> List[PersistedAgentState]:
340
- with self.session_maker() as session:
341
- results = session.query(AgentModel).filter(AgentModel.user_id == user_id).all()
342
- return [r.to_record() for r in results]
343
-
344
- @enforce_types
345
- def get_agent(
346
- self, agent_id: Optional[str] = None, agent_name: Optional[str] = None, user_id: Optional[str] = None
347
- ) -> Optional[PersistedAgentState]:
348
- with self.session_maker() as session:
349
- if agent_id:
350
- results = session.query(AgentModel).filter(AgentModel.id == agent_id).all()
351
- else:
352
- assert agent_name is not None and user_id is not None, "Must provide either agent_id or agent_name"
353
- results = session.query(AgentModel).filter(AgentModel.name == agent_name).filter(AgentModel.user_id == user_id).all()
354
-
355
- if len(results) == 0:
356
- return None
357
- assert len(results) == 1, f"Expected 1 result, got {len(results)}" # should only be one result
358
- return results[0].to_record()
359
-
360
- # agent source metadata
361
- @enforce_types
362
- def attach_source(self, user_id: str, agent_id: str, source_id: str):
363
- with self.session_maker() as session:
364
- # TODO: remove this (is a hack)
365
- mapping_id = f"{user_id}-{agent_id}-{source_id}"
366
- existing = session.query(AgentSourceMappingModel).filter(
367
- AgentSourceMappingModel.id == mapping_id
368
- ).first()
369
-
370
- if existing is None:
371
- # Only create if it doesn't exist
372
- session.add(AgentSourceMappingModel(
373
- id=mapping_id,
374
- user_id=user_id,
375
- agent_id=agent_id,
376
- source_id=source_id
377
- ))
378
- session.commit()
379
-
380
- @enforce_types
381
- def list_attached_source_ids(self, agent_id: str) -> List[str]:
382
- with self.session_maker() as session:
383
- results = session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.agent_id == agent_id).all()
384
- return [r.source_id for r in results]
385
-
386
- @enforce_types
387
- def list_attached_agents(self, source_id: str) -> List[str]:
388
- with self.session_maker() as session:
389
- results = session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.source_id == source_id).all()
390
-
391
- agent_ids = []
392
- # make sure agent exists
393
- for r in results:
394
- agent = self.get_agent(agent_id=r.agent_id)
395
- if agent:
396
- agent_ids.append(r.agent_id)
397
- else:
398
- printd(f"Warning: agent {r.agent_id} does not exist but exists in mapping database. This should never happen.")
399
- return agent_ids
400
-
401
- @enforce_types
402
- def detach_source(self, agent_id: str, source_id: str):
403
- with self.session_maker() as session:
404
- session.query(AgentSourceMappingModel).filter(
405
- AgentSourceMappingModel.agent_id == agent_id, AgentSourceMappingModel.source_id == source_id
406
- ).delete()
407
- session.commit()
@@ -1,33 +0,0 @@
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/api_key.py DELETED
@@ -1,21 +0,0 @@
1
- from typing import Optional
2
-
3
- from pydantic import Field
4
-
5
- from letta.schemas.letta_base import LettaBase
6
-
7
-
8
- class BaseAPIKey(LettaBase):
9
- __id_prefix__ = "sk" # secret key
10
-
11
-
12
- class APIKey(BaseAPIKey):
13
- id: str = BaseAPIKey.generate_id_field()
14
- user_id: str = Field(..., description="The unique identifier of the user associated with the token.")
15
- key: str = Field(..., description="The key value.")
16
- name: str = Field(..., description="Name of the token.")
17
-
18
-
19
- class APIKeyCreate(BaseAPIKey):
20
- user_id: str = Field(..., description="The unique identifier of the user associated with the token.")
21
- name: Optional[str] = Field(None, description="Name of the token.")
@@ -1,32 +0,0 @@
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 BlocksAgentsBase(LettaBase):
10
- __id_prefix__ = "blocks_agents"
11
-
12
-
13
- class BlocksAgents(BlocksAgentsBase):
14
- """
15
- Schema representing the relationship between blocks and agents.
16
-
17
- Parameters:
18
- agent_id (str): The ID of the associated agent.
19
- block_id (str): The ID of the associated block.
20
- block_label (str): The label of the block.
21
- created_at (datetime): The date this relationship was created.
22
- updated_at (datetime): The date this relationship was last updated.
23
- is_deleted (bool): Whether this block-agent relationship is deleted or not.
24
- """
25
-
26
- id: str = BlocksAgentsBase.generate_id_field()
27
- agent_id: str = Field(..., description="The ID of the associated agent.")
28
- block_id: str = Field(..., description="The ID of the associated block.")
29
- block_label: str = Field(..., description="The label of the block.")
30
- created_at: Optional[datetime] = Field(None, description="The creation date of the association.")
31
- updated_at: Optional[datetime] = Field(None, description="The update date of the association.")
32
- is_deleted: bool = Field(False, description="Whether this block-agent relationship is deleted or not.")
@@ -1,32 +0,0 @@
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 ToolsAgentsBase(LettaBase):
10
- __id_prefix__ = "tools_agents"
11
-
12
-
13
- class ToolsAgents(ToolsAgentsBase):
14
- """
15
- Schema representing the relationship between tools and agents.
16
-
17
- Parameters:
18
- agent_id (str): The ID of the associated agent.
19
- tool_id (str): The ID of the associated tool.
20
- tool_name (str): The name of the tool.
21
- created_at (datetime): The date this relationship was created.
22
- updated_at (datetime): The date this relationship was last updated.
23
- is_deleted (bool): Whether this tool-agent relationship is deleted or not.
24
- """
25
-
26
- id: str = ToolsAgentsBase.generate_id_field()
27
- agent_id: str = Field(..., description="The ID of the associated agent.")
28
- tool_id: str = Field(..., description="The ID of the associated tool.")
29
- tool_name: str = Field(..., description="The name of the tool.")
30
- created_at: Optional[datetime] = Field(None, description="The creation date of the association.")
31
- updated_at: Optional[datetime] = Field(None, description="The update date of the association.")
32
- is_deleted: bool = Field(False, description="Whether this tool-agent relationship is deleted or not.")