letta-nightly 0.6.12.dev20250122104013__py3-none-any.whl → 0.6.14.dev20250123041709__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 (61) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +69 -100
  3. letta/chat_only_agent.py +1 -1
  4. letta/client/client.py +169 -149
  5. letta/constants.py +1 -8
  6. letta/data_sources/connectors.py +1 -1
  7. letta/functions/helpers.py +29 -4
  8. letta/functions/schema_generator.py +55 -0
  9. letta/llm_api/helpers.py +51 -1
  10. letta/memory.py +9 -7
  11. letta/orm/agent.py +2 -2
  12. letta/orm/block.py +3 -1
  13. letta/orm/custom_columns.py +5 -4
  14. letta/orm/enums.py +1 -0
  15. letta/orm/message.py +2 -2
  16. letta/orm/sqlalchemy_base.py +5 -0
  17. letta/schemas/agent.py +13 -13
  18. letta/schemas/block.py +2 -2
  19. letta/schemas/environment_variables.py +1 -1
  20. letta/schemas/job.py +1 -1
  21. letta/schemas/letta_base.py +6 -0
  22. letta/schemas/letta_message.py +6 -6
  23. letta/schemas/memory.py +3 -2
  24. letta/schemas/message.py +21 -13
  25. letta/schemas/passage.py +1 -1
  26. letta/schemas/source.py +4 -4
  27. letta/schemas/tool.py +38 -43
  28. letta/server/rest_api/app.py +1 -16
  29. letta/server/rest_api/routers/v1/agents.py +95 -118
  30. letta/server/rest_api/routers/v1/blocks.py +8 -46
  31. letta/server/rest_api/routers/v1/jobs.py +4 -4
  32. letta/server/rest_api/routers/v1/providers.py +2 -2
  33. letta/server/rest_api/routers/v1/runs.py +6 -6
  34. letta/server/rest_api/routers/v1/sources.py +8 -38
  35. letta/server/rest_api/routers/v1/tags.py +1 -1
  36. letta/server/rest_api/routers/v1/tools.py +6 -24
  37. letta/server/server.py +6 -6
  38. letta/services/agent_manager.py +43 -9
  39. letta/services/block_manager.py +3 -3
  40. letta/services/job_manager.py +5 -3
  41. letta/services/organization_manager.py +1 -1
  42. letta/services/passage_manager.py +3 -3
  43. letta/services/provider_manager.py +2 -2
  44. letta/services/sandbox_config_manager.py +2 -2
  45. letta/services/source_manager.py +3 -3
  46. letta/services/tool_execution_sandbox.py +3 -1
  47. letta/services/tool_manager.py +8 -3
  48. letta/services/user_manager.py +2 -2
  49. letta/settings.py +29 -0
  50. letta/system.py +2 -2
  51. {letta_nightly-0.6.12.dev20250122104013.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/METADATA +1 -1
  52. {letta_nightly-0.6.12.dev20250122104013.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/RECORD +55 -61
  53. letta/server/rest_api/routers/openai/__init__.py +0 -0
  54. letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
  55. letta/server/rest_api/routers/openai/assistants/assistants.py +0 -115
  56. letta/server/rest_api/routers/openai/assistants/schemas.py +0 -115
  57. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  58. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -120
  59. {letta_nightly-0.6.12.dev20250122104013.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/LICENSE +0 -0
  60. {letta_nightly-0.6.12.dev20250122104013.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/WHEEL +0 -0
  61. {letta_nightly-0.6.12.dev20250122104013.dist-info → letta_nightly-0.6.14.dev20250123041709.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, List, Optional
2
2
 
3
- from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Response
3
+ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
4
4
 
5
5
  from letta.orm.errors import NoResultFound
6
6
  from letta.schemas.block import Block, BlockUpdate, CreateBlock
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
13
13
  router = APIRouter(prefix="/blocks", tags=["blocks"])
14
14
 
15
15
 
16
- @router.get("/", response_model=List[Block], operation_id="list_memory_blocks")
16
+ @router.get("/", response_model=List[Block], operation_id="list_blocks")
17
17
  def list_blocks(
18
18
  # query parameters
19
19
  label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"),
@@ -26,7 +26,7 @@ def list_blocks(
26
26
  return server.block_manager.get_blocks(actor=actor, label=label, is_template=templates_only, template_name=name)
27
27
 
28
28
 
29
- @router.post("/", response_model=Block, operation_id="create_memory_block")
29
+ @router.post("/", response_model=Block, operation_id="create_block")
30
30
  def create_block(
31
31
  create_block: CreateBlock = Body(...),
32
32
  server: SyncServer = Depends(get_letta_server),
@@ -37,8 +37,8 @@ def create_block(
37
37
  return server.block_manager.create_or_update_block(actor=actor, block=block)
38
38
 
39
39
 
40
- @router.patch("/{block_id}", response_model=Block, operation_id="update_memory_block")
41
- def update_block(
40
+ @router.patch("/{block_id}", response_model=Block, operation_id="modify_block")
41
+ def modify_block(
42
42
  block_id: str,
43
43
  block_update: BlockUpdate = Body(...),
44
44
  server: SyncServer = Depends(get_letta_server),
@@ -48,7 +48,7 @@ def update_block(
48
48
  return server.block_manager.update_block(block_id=block_id, block_update=block_update, actor=actor)
49
49
 
50
50
 
51
- @router.delete("/{block_id}", response_model=Block, operation_id="delete_memory_block")
51
+ @router.delete("/{block_id}", response_model=Block, operation_id="delete_block")
52
52
  def delete_block(
53
53
  block_id: str,
54
54
  server: SyncServer = Depends(get_letta_server),
@@ -58,8 +58,8 @@ def delete_block(
58
58
  return server.block_manager.delete_block(block_id=block_id, actor=actor)
59
59
 
60
60
 
61
- @router.get("/{block_id}", response_model=Block, operation_id="get_memory_block")
62
- def get_block(
61
+ @router.get("/{block_id}", response_model=Block, operation_id="retrieve_block")
62
+ def retrieve_block(
63
63
  block_id: str,
64
64
  server: SyncServer = Depends(get_letta_server),
65
65
  user_id: Optional[str] = Header(None, alias="user_id"),
@@ -73,41 +73,3 @@ def get_block(
73
73
  return block
74
74
  except NoResultFound:
75
75
  raise HTTPException(status_code=404, detail="Block not found")
76
-
77
-
78
- @router.patch("/{block_id}/attach", response_model=None, status_code=204, operation_id="link_agent_memory_block")
79
- def link_agent_memory_block(
80
- block_id: str,
81
- agent_id: str = Query(..., description="The unique identifier of the agent to attach the source to."),
82
- server: "SyncServer" = Depends(get_letta_server),
83
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
84
- ):
85
- """
86
- Link a memory block to an agent.
87
- """
88
- actor = server.user_manager.get_user_or_default(user_id=user_id)
89
-
90
- try:
91
- server.agent_manager.attach_block(agent_id=agent_id, block_id=block_id, actor=actor)
92
- return Response(status_code=204)
93
- except NoResultFound as e:
94
- raise HTTPException(status_code=404, detail=str(e))
95
-
96
-
97
- @router.patch("/{block_id}/detach", response_model=None, status_code=204, operation_id="unlink_agent_memory_block")
98
- def unlink_agent_memory_block(
99
- block_id: str,
100
- agent_id: str = Query(..., description="The unique identifier of the agent to attach the source to."),
101
- server: "SyncServer" = Depends(get_letta_server),
102
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
103
- ):
104
- """
105
- Unlink a memory block from an agent
106
- """
107
- actor = server.user_manager.get_user_or_default(user_id=user_id)
108
-
109
- try:
110
- server.agent_manager.detach_block(agent_id=agent_id, block_id=block_id, actor=actor)
111
- return Response(status_code=204)
112
- except NoResultFound as e:
113
- raise HTTPException(status_code=404, detail=str(e))
@@ -26,9 +26,9 @@ def list_jobs(
26
26
  jobs = server.job_manager.list_jobs(actor=actor)
27
27
 
28
28
  if source_id:
29
- # can't be in the ORM since we have source_id stored in the metadata_
29
+ # can't be in the ORM since we have source_id stored in the metadata
30
30
  # TODO: Probably change this
31
- jobs = [job for job in jobs if job.metadata_.get("source_id") == source_id]
31
+ jobs = [job for job in jobs if job.metadata.get("source_id") == source_id]
32
32
  return jobs
33
33
 
34
34
 
@@ -45,8 +45,8 @@ def list_active_jobs(
45
45
  return server.job_manager.list_jobs(actor=actor, statuses=[JobStatus.created, JobStatus.running])
46
46
 
47
47
 
48
- @router.get("/{job_id}", response_model=Job, operation_id="get_job")
49
- def get_job(
48
+ @router.get("/{job_id}", response_model=Job, operation_id="retrieve_job")
49
+ def retrieve_job(
50
50
  job_id: str,
51
51
  user_id: Optional[str] = Header(None, alias="user_id"),
52
52
  server: "SyncServer" = Depends(get_letta_server),
@@ -45,8 +45,8 @@ def create_provider(
45
45
  return provider
46
46
 
47
47
 
48
- @router.put("/", tags=["providers"], response_model=Provider, operation_id="update_provider")
49
- def update_provider(
48
+ @router.patch("/", tags=["providers"], response_model=Provider, operation_id="modify_provider")
49
+ def modify_provider(
50
50
  request: ProviderUpdate = Body(...),
51
51
  server: "SyncServer" = Depends(get_letta_server),
52
52
  ):
@@ -43,8 +43,8 @@ def list_active_runs(
43
43
  return [Run.from_job(job) for job in active_runs]
44
44
 
45
45
 
46
- @router.get("/{run_id}", response_model=Run, operation_id="get_run")
47
- def get_run(
46
+ @router.get("/{run_id}", response_model=Run, operation_id="retrieve_run")
47
+ def retrieve_run(
48
48
  run_id: str,
49
49
  user_id: Optional[str] = Header(None, alias="user_id"),
50
50
  server: "SyncServer" = Depends(get_letta_server),
@@ -69,9 +69,9 @@ RunMessagesResponse = Annotated[
69
69
  @router.get(
70
70
  "/{run_id}/messages",
71
71
  response_model=RunMessagesResponse,
72
- operation_id="get_run_messages",
72
+ operation_id="list_run_messages",
73
73
  )
74
- async def get_run_messages(
74
+ async def list_run_messages(
75
75
  run_id: str,
76
76
  server: "SyncServer" = Depends(get_letta_server),
77
77
  user_id: Optional[str] = Header(None, alias="user_id"),
@@ -111,8 +111,8 @@ async def get_run_messages(
111
111
  raise HTTPException(status_code=404, detail=str(e))
112
112
 
113
113
 
114
- @router.get("/{run_id}/usage", response_model=UsageStatistics, operation_id="get_run_usage")
115
- def get_run_usage(
114
+ @router.get("/{run_id}/usage", response_model=UsageStatistics, operation_id="retrieve_run_usage")
115
+ def retrieve_run_usage(
116
116
  run_id: str,
117
117
  user_id: Optional[str] = Header(None, alias="user_id"),
118
118
  server: "SyncServer" = Depends(get_letta_server),
@@ -19,8 +19,8 @@ from letta.utils import sanitize_filename
19
19
  router = APIRouter(prefix="/sources", tags=["sources"])
20
20
 
21
21
 
22
- @router.get("/{source_id}", response_model=Source, operation_id="get_source")
23
- def get_source(
22
+ @router.get("/{source_id}", response_model=Source, operation_id="retrieve_source")
23
+ def retrieve_source(
24
24
  source_id: str,
25
25
  server: "SyncServer" = Depends(get_letta_server),
26
26
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -81,8 +81,8 @@ def create_source(
81
81
  return server.source_manager.create_source(source=source, actor=actor)
82
82
 
83
83
 
84
- @router.patch("/{source_id}", response_model=Source, operation_id="update_source")
85
- def update_source(
84
+ @router.patch("/{source_id}", response_model=Source, operation_id="modify_source")
85
+ def modify_source(
86
86
  source_id: str,
87
87
  source: SourceUpdate,
88
88
  server: "SyncServer" = Depends(get_letta_server),
@@ -111,36 +111,6 @@ def delete_source(
111
111
  server.delete_source(source_id=source_id, actor=actor)
112
112
 
113
113
 
114
- @router.post("/{source_id}/attach", response_model=Source, operation_id="attach_agent_to_source")
115
- def attach_source_to_agent(
116
- source_id: str,
117
- agent_id: str = Query(..., description="The unique identifier of the agent to attach the source to."),
118
- server: "SyncServer" = Depends(get_letta_server),
119
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
120
- ):
121
- """
122
- Attach a data source to an existing agent.
123
- """
124
- actor = server.user_manager.get_user_or_default(user_id=user_id)
125
- server.agent_manager.attach_source(source_id=source_id, agent_id=agent_id, actor=actor)
126
- return server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
127
-
128
-
129
- @router.post("/{source_id}/detach", response_model=Source, operation_id="detach_agent_from_source")
130
- def detach_source_from_agent(
131
- source_id: str,
132
- agent_id: str = Query(..., description="The unique identifier of the agent to detach the source from."),
133
- server: "SyncServer" = Depends(get_letta_server),
134
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
135
- ) -> None:
136
- """
137
- Detach a data source from an existing agent.
138
- """
139
- actor = server.user_manager.get_user_or_default(user_id=user_id)
140
- server.agent_manager.detach_source(agent_id=agent_id, source_id=source_id, actor=actor)
141
- return server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
142
-
143
-
144
114
  @router.post("/{source_id}/upload", response_model=Job, operation_id="upload_file_to_source")
145
115
  def upload_file_to_source(
146
116
  file: UploadFile,
@@ -161,7 +131,7 @@ def upload_file_to_source(
161
131
  # create job
162
132
  job = Job(
163
133
  user_id=actor.id,
164
- metadata_={"type": "embedding", "filename": file.filename, "source_id": source_id},
134
+ metadata={"type": "embedding", "filename": file.filename, "source_id": source_id},
165
135
  completed_at=None,
166
136
  )
167
137
  job_id = job.id
@@ -178,7 +148,7 @@ def upload_file_to_source(
178
148
 
179
149
 
180
150
  @router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages")
181
- def list_passages(
151
+ def list_source_passages(
182
152
  source_id: str,
183
153
  server: SyncServer = Depends(get_letta_server),
184
154
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -191,8 +161,8 @@ def list_passages(
191
161
  return passages
192
162
 
193
163
 
194
- @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_files_from_source")
195
- def list_files_from_source(
164
+ @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_source_files")
165
+ def list_source_files(
196
166
  source_id: str,
197
167
  limit: int = Query(1000, description="Number of files to return"),
198
168
  cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
@@ -12,7 +12,7 @@ router = APIRouter(prefix="/tags", tags=["tag", "admin"])
12
12
 
13
13
 
14
14
  @router.get("/", tags=["admin"], response_model=List[str], operation_id="list_tags")
15
- def get_tags(
15
+ def list_tags(
16
16
  cursor: Optional[str] = Query(None),
17
17
  limit: Optional[int] = Query(50),
18
18
  server: "SyncServer" = Depends(get_letta_server),
@@ -31,8 +31,8 @@ def delete_tool(
31
31
  server.tool_manager.delete_tool_by_id(tool_id=tool_id, actor=actor)
32
32
 
33
33
 
34
- @router.get("/{tool_id}", response_model=Tool, operation_id="get_tool")
35
- def get_tool(
34
+ @router.get("/{tool_id}", response_model=Tool, operation_id="retrieve_tool")
35
+ def retrieve_tool(
36
36
  tool_id: str,
37
37
  server: SyncServer = Depends(get_letta_server),
38
38
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -48,23 +48,6 @@ def get_tool(
48
48
  return tool
49
49
 
50
50
 
51
- @router.get("/name/{tool_name}", response_model=str, operation_id="get_tool_id_by_name")
52
- def get_tool_id(
53
- tool_name: str,
54
- server: SyncServer = Depends(get_letta_server),
55
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
56
- ):
57
- """
58
- Get a tool ID by name
59
- """
60
- actor = server.user_manager.get_user_or_default(user_id=user_id)
61
- tool = server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
62
- if tool:
63
- return tool.id
64
- else:
65
- raise HTTPException(status_code=404, detail=f"Tool with name {tool_name} and organization id {actor.organization_id} not found.")
66
-
67
-
68
51
  @router.get("/", response_model=List[Tool], operation_id="list_tools")
69
52
  def list_tools(
70
53
  cursor: Optional[str] = None,
@@ -139,8 +122,8 @@ def upsert_tool(
139
122
  raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
140
123
 
141
124
 
142
- @router.patch("/{tool_id}", response_model=Tool, operation_id="update_tool")
143
- def update_tool(
125
+ @router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool")
126
+ def modify_tool(
144
127
  tool_id: str,
145
128
  request: ToolUpdate = Body(...),
146
129
  server: SyncServer = Depends(get_letta_server),
@@ -237,11 +220,10 @@ def add_composio_tool(
237
220
  Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
238
221
  """
239
222
  actor = server.user_manager.get_user_or_default(user_id=user_id)
240
- composio_api_key = get_composio_key(server, actor=actor)
241
223
 
242
224
  try:
243
- tool_create = ToolCreate.from_composio(action_name=composio_action_name, api_key=composio_api_key)
244
- return server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=actor)
225
+ tool_create = ToolCreate.from_composio(action_name=composio_action_name)
226
+ return server.tool_manager.create_or_update_composio_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=actor)
245
227
  except EnumStringNotFound as e:
246
228
  raise HTTPException(
247
229
  status_code=400, # Bad Request
letta/server/server.py CHANGED
@@ -773,9 +773,9 @@ class SyncServer(Server):
773
773
  interface: Union[AgentInterface, None] = None,
774
774
  ) -> AgentState:
775
775
  if request.llm_config is None:
776
- if request.llm is None:
777
- raise ValueError("Must specify either llm or llm_config in request")
778
- request.llm_config = self.get_llm_config_from_handle(handle=request.llm, context_window_limit=request.context_window_limit)
776
+ if request.model is None:
777
+ raise ValueError("Must specify either model or llm_config in request")
778
+ request.llm_config = self.get_llm_config_from_handle(handle=request.model, context_window_limit=request.context_window_limit)
779
779
 
780
780
  if request.embedding_config is None:
781
781
  if request.embedding is None:
@@ -956,8 +956,8 @@ class SyncServer(Server):
956
956
 
957
957
  # update job status
958
958
  job.status = JobStatus.completed
959
- job.metadata_["num_passages"] = num_passages
960
- job.metadata_["num_documents"] = num_documents
959
+ job.metadata["num_passages"] = num_passages
960
+ job.metadata["num_documents"] = num_documents
961
961
  self.job_manager.update_job_by_id(job_id=job_id, job_update=JobUpdate(**job.model_dump()), actor=actor)
962
962
 
963
963
  # update all agents who have this source attached
@@ -1019,7 +1019,7 @@ class SyncServer(Server):
1019
1019
  attached_agents = [{"id": agent.id, "name": agent.name} for agent in agents]
1020
1020
 
1021
1021
  # Overwrite metadata field, should be empty anyways
1022
- source.metadata_ = dict(
1022
+ source.metadata = dict(
1023
1023
  num_documents=num_documents,
1024
1024
  num_passages=num_passages,
1025
1025
  attached_agents=attached_agents,
@@ -25,6 +25,7 @@ from letta.schemas.message import Message as PydanticMessage
25
25
  from letta.schemas.message import MessageCreate
26
26
  from letta.schemas.passage import Passage as PydanticPassage
27
27
  from letta.schemas.source import Source as PydanticSource
28
+ from letta.schemas.tool import Tool as PydanticTool
28
29
  from letta.schemas.tool_rule import ToolRule as PydanticToolRule
29
30
  from letta.schemas.user import User as PydanticUser
30
31
  from letta.services.block_manager import BlockManager
@@ -81,7 +82,7 @@ class AgentManager:
81
82
  block_ids = list(agent_create.block_ids or []) # Create a local copy to avoid modifying the original
82
83
  if agent_create.memory_blocks:
83
84
  for create_block in agent_create.memory_blocks:
84
- block = self.block_manager.create_or_update_block(PydanticBlock(**create_block.model_dump()), actor=actor)
85
+ block = self.block_manager.create_or_update_block(PydanticBlock(**create_block.model_dump(to_orm=True)), actor=actor)
85
86
  block_ids.append(block.id)
86
87
 
87
88
  # TODO: Remove this block once we deprecate the legacy `tools` field
@@ -116,7 +117,7 @@ class AgentManager:
116
117
  source_ids=agent_create.source_ids or [],
117
118
  tags=agent_create.tags or [],
118
119
  description=agent_create.description,
119
- metadata_=agent_create.metadata_,
120
+ metadata=agent_create.metadata,
120
121
  tool_rules=agent_create.tool_rules,
121
122
  actor=actor,
122
123
  )
@@ -176,7 +177,7 @@ class AgentManager:
176
177
  source_ids: List[str],
177
178
  tags: List[str],
178
179
  description: Optional[str] = None,
179
- metadata_: Optional[Dict] = None,
180
+ metadata: Optional[Dict] = None,
180
181
  tool_rules: Optional[List[PydanticToolRule]] = None,
181
182
  ) -> PydanticAgentState:
182
183
  """Create a new agent."""
@@ -190,7 +191,7 @@ class AgentManager:
190
191
  "embedding_config": embedding_config,
191
192
  "organization_id": actor.organization_id,
192
193
  "description": description,
193
- "metadata_": metadata_,
194
+ "metadata_": metadata,
194
195
  "tool_rules": tool_rules,
195
196
  }
196
197
 
@@ -241,11 +242,14 @@ class AgentManager:
241
242
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
242
243
 
243
244
  # Update scalar fields directly
244
- scalar_fields = {"name", "system", "llm_config", "embedding_config", "message_ids", "tool_rules", "description", "metadata_"}
245
+ scalar_fields = {"name", "system", "llm_config", "embedding_config", "message_ids", "tool_rules", "description", "metadata"}
245
246
  for field in scalar_fields:
246
247
  value = getattr(agent_update, field, None)
247
248
  if value is not None:
248
- setattr(agent, field, value)
249
+ if field == "metadata":
250
+ setattr(agent, "metadata_", value)
251
+ else:
252
+ setattr(agent, field, value)
249
253
 
250
254
  # Update relationships using _process_relationship and _process_tags
251
255
  if agent_update.tool_ids is not None:
@@ -464,6 +468,12 @@ class AgentManager:
464
468
  new_messages = [message_ids[0]] + message_ids[num:] # 0 is system message
465
469
  return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
466
470
 
471
+ @enforce_types
472
+ def trim_all_in_context_messages_except_system(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
473
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
474
+ new_messages = [message_ids[0]] # 0 is system message
475
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
476
+
467
477
  @enforce_types
468
478
  def prepend_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
469
479
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
@@ -531,7 +541,7 @@ class AgentManager:
531
541
  # Source Management
532
542
  # ======================================================================================================================
533
543
  @enforce_types
534
- def attach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> None:
544
+ def attach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
535
545
  """
536
546
  Attaches a source to an agent.
537
547
 
@@ -561,6 +571,7 @@ class AgentManager:
561
571
 
562
572
  # Commit the changes
563
573
  agent.update(session, actor=actor)
574
+ return agent.to_pydantic()
564
575
 
565
576
  @enforce_types
566
577
  def list_attached_sources(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
@@ -582,7 +593,7 @@ class AgentManager:
582
593
  return [source.to_pydantic() for source in agent.sources]
583
594
 
584
595
  @enforce_types
585
- def detach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> None:
596
+ def detach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
586
597
  """
587
598
  Detaches a source from an agent.
588
599
 
@@ -596,10 +607,17 @@ class AgentManager:
596
607
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
597
608
 
598
609
  # Remove the source from the relationship
599
- agent.sources = [s for s in agent.sources if s.id != source_id]
610
+ remaining_sources = [s for s in agent.sources if s.id != source_id]
611
+
612
+ if len(remaining_sources) == len(agent.sources): # Source ID was not in the relationship
613
+ logger.warning(f"Attempted to remove unattached source id={source_id} from agent id={agent_id} by actor={actor}")
614
+
615
+ # Update the sources relationship
616
+ agent.sources = remaining_sources
600
617
 
601
618
  # Commit the changes
602
619
  agent.update(session, actor=actor)
620
+ return agent.to_pydantic()
603
621
 
604
622
  # ======================================================================================================================
605
623
  # Block management
@@ -1005,6 +1023,22 @@ class AgentManager:
1005
1023
  agent.update(session, actor=actor)
1006
1024
  return agent.to_pydantic()
1007
1025
 
1026
+ @enforce_types
1027
+ def list_attached_tools(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
1028
+ """
1029
+ List all tools attached to an agent.
1030
+
1031
+ Args:
1032
+ agent_id: ID of the agent to list tools for.
1033
+ actor: User performing the action.
1034
+
1035
+ Returns:
1036
+ List[PydanticTool]: List of tools attached to the agent.
1037
+ """
1038
+ with self.session_maker() as session:
1039
+ agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1040
+ return [tool.to_pydantic() for tool in agent.tools]
1041
+
1008
1042
  # ======================================================================================================================
1009
1043
  # Tag Management
1010
1044
  # ======================================================================================================================
@@ -24,11 +24,11 @@ class BlockManager:
24
24
  """Create a new block based on the Block schema."""
25
25
  db_block = self.get_block_by_id(block.id, actor)
26
26
  if db_block:
27
- update_data = BlockUpdate(**block.model_dump(exclude_none=True))
27
+ update_data = BlockUpdate(**block.model_dump(to_orm=True, exclude_none=True))
28
28
  self.update_block(block.id, update_data, actor)
29
29
  else:
30
30
  with self.session_maker() as session:
31
- data = block.model_dump(exclude_none=True)
31
+ data = block.model_dump(to_orm=True, exclude_none=True)
32
32
  block = BlockModel(**data, organization_id=actor.organization_id)
33
33
  block.create(session, actor=actor)
34
34
  return block.to_pydantic()
@@ -40,7 +40,7 @@ class BlockManager:
40
40
 
41
41
  with self.session_maker() as session:
42
42
  block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
43
- update_data = block_update.model_dump(exclude_unset=True, exclude_none=True)
43
+ update_data = block_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
44
44
 
45
45
  for key, value in update_data.items():
46
46
  setattr(block, key, value)
@@ -39,7 +39,7 @@ class JobManager:
39
39
  with self.session_maker() as session:
40
40
  # Associate the job with the user
41
41
  pydantic_job.user_id = actor.id
42
- job_data = pydantic_job.model_dump()
42
+ job_data = pydantic_job.model_dump(to_orm=True)
43
43
  job = JobModel(**job_data)
44
44
  job.create(session, actor=actor) # Save job in the database
45
45
  return job.to_pydantic()
@@ -52,7 +52,7 @@ class JobManager:
52
52
  job = self._verify_job_access(session=session, job_id=job_id, actor=actor, access=["write"])
53
53
 
54
54
  # Update job attributes with only the fields that were explicitly set
55
- update_data = job_update.model_dump(exclude_unset=True, exclude_none=True)
55
+ update_data = job_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
56
56
 
57
57
  # Automatically update the completion timestamp if status is set to 'completed'
58
58
  if update_data.get("status") == JobStatus.completed and not job.completed_at:
@@ -62,7 +62,9 @@ class JobManager:
62
62
  setattr(job, key, value)
63
63
 
64
64
  # Save the updated job to the database
65
- return job.update(db_session=session) # TODO: Add this later , actor=actor)
65
+ job.update(db_session=session) # TODO: Add this later , actor=actor)
66
+
67
+ return job.to_pydantic()
66
68
 
67
69
  @enforce_types
68
70
  def get_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
@@ -44,7 +44,7 @@ class OrganizationManager:
44
44
  @enforce_types
45
45
  def _create_organization(self, pydantic_org: PydanticOrganization) -> PydanticOrganization:
46
46
  with self.session_maker() as session:
47
- org = OrganizationModel(**pydantic_org.model_dump())
47
+ org = OrganizationModel(**pydantic_org.model_dump(to_orm=True))
48
48
  org.create(session)
49
49
  return org.to_pydantic()
50
50
 
@@ -38,14 +38,14 @@ class PassageManager:
38
38
  def create_passage(self, pydantic_passage: PydanticPassage, actor: PydanticUser) -> PydanticPassage:
39
39
  """Create a new passage in the appropriate table based on whether it has agent_id or source_id."""
40
40
  # Common fields for both passage types
41
- data = pydantic_passage.model_dump()
41
+ data = pydantic_passage.model_dump(to_orm=True)
42
42
  common_fields = {
43
43
  "id": data.get("id"),
44
44
  "text": data["text"],
45
45
  "embedding": data["embedding"],
46
46
  "embedding_config": data["embedding_config"],
47
47
  "organization_id": data["organization_id"],
48
- "metadata_": data.get("metadata_", {}),
48
+ "metadata_": data.get("metadata", {}),
49
49
  "is_deleted": data.get("is_deleted", False),
50
50
  "created_at": data.get("created_at", datetime.utcnow()),
51
51
  }
@@ -145,7 +145,7 @@ class PassageManager:
145
145
  raise ValueError(f"Passage with id {passage_id} does not exist.")
146
146
 
147
147
  # Update the database record with values from the provided record
148
- update_data = passage.model_dump(exclude_unset=True, exclude_none=True)
148
+ update_data = passage.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
149
149
  for key, value in update_data.items():
150
150
  setattr(curr_passage, key, value)
151
151
 
@@ -24,7 +24,7 @@ class ProviderManager:
24
24
  # Lazily create the provider id prior to persistence
25
25
  provider.resolve_identifier()
26
26
 
27
- new_provider = ProviderModel(**provider.model_dump(exclude_unset=True))
27
+ new_provider = ProviderModel(**provider.model_dump(to_orm=True, exclude_unset=True))
28
28
  new_provider.create(session)
29
29
  return new_provider.to_pydantic()
30
30
 
@@ -36,7 +36,7 @@ class ProviderManager:
36
36
  existing_provider = ProviderModel.read(db_session=session, identifier=provider_update.id)
37
37
 
38
38
  # Update only the fields that are provided in ProviderUpdate
39
- update_data = provider_update.model_dump(exclude_unset=True, exclude_none=True)
39
+ update_data = provider_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
40
40
  for key, value in update_data.items():
41
41
  setattr(existing_provider, key, value)
42
42
 
@@ -172,7 +172,7 @@ class SandboxConfigManager:
172
172
  return db_env_var
173
173
  else:
174
174
  with self.session_maker() as session:
175
- env_var = SandboxEnvVarModel(**env_var.model_dump(exclude_none=True))
175
+ env_var = SandboxEnvVarModel(**env_var.model_dump(to_orm=True, exclude_none=True))
176
176
  env_var.create(session, actor=actor)
177
177
  return env_var.to_pydantic()
178
178
 
@@ -183,7 +183,7 @@ class SandboxConfigManager:
183
183
  """Update an existing sandbox environment variable."""
184
184
  with self.session_maker() as session:
185
185
  env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
186
- update_data = env_var_update.model_dump(exclude_unset=True, exclude_none=True)
186
+ update_data = env_var_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
187
187
  update_data = {key: value for key, value in update_data.items() if getattr(env_var, key) != value}
188
188
 
189
189
  if update_data:
@@ -30,7 +30,7 @@ class SourceManager:
30
30
  with self.session_maker() as session:
31
31
  # Provide default embedding config if not given
32
32
  source.organization_id = actor.organization_id
33
- source = SourceModel(**source.model_dump(exclude_none=True))
33
+ source = SourceModel(**source.model_dump(to_orm=True, exclude_none=True))
34
34
  source.create(session, actor=actor)
35
35
  return source.to_pydantic()
36
36
 
@@ -41,7 +41,7 @@ class SourceManager:
41
41
  source = SourceModel.read(db_session=session, identifier=source_id, actor=actor)
42
42
 
43
43
  # get update dictionary
44
- update_data = source_update.model_dump(exclude_unset=True, exclude_none=True)
44
+ update_data = source_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
45
45
  # Remove redundant update fields
46
46
  update_data = {key: value for key, value in update_data.items() if getattr(source, key) != value}
47
47
 
@@ -132,7 +132,7 @@ class SourceManager:
132
132
  else:
133
133
  with self.session_maker() as session:
134
134
  file_metadata.organization_id = actor.organization_id
135
- file_metadata = FileMetadataModel(**file_metadata.model_dump(exclude_none=True))
135
+ file_metadata = FileMetadataModel(**file_metadata.model_dump(to_orm=True, exclude_none=True))
136
136
  file_metadata.create(session, actor=actor)
137
137
  return file_metadata.to_pydantic()
138
138