feat: add validation to fastapi routes for agent IDs (#5454)

* change my PR to match Caren's

* add path parameter validation for agent id first

* remove old import

* remove old agent_id_pattern pattern

* add example and fix max/min calculation to include hyphen

* fix regex string interpolation

* example deprecated in favour of examples

* openapi autogen

* change template test to expect 422

* fix 422 swallow

* expect 422 or 400

* rewrite  error codes

* fix hallucinated uuid

* tweaked error message test

* print docker logs on failure
This commit is contained in:
Kian Jones
2025-10-16 16:49:28 -07:00
committed by Caren Thomas
parent 505c9cff57
commit 4bbd760204
7 changed files with 534 additions and 225 deletions

View File

@@ -3755,8 +3755,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "max_steps",
@@ -3885,8 +3891,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -3926,8 +3938,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -3975,8 +3993,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "include_relationships",
@@ -4035,8 +4059,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4074,8 +4104,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "before",
@@ -4194,15 +4230,6 @@
"description": "Attach a tool to an agent.",
"operationId": "attach_tool",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "tool_id",
"in": "path",
@@ -4211,6 +4238,21 @@
"type": "string",
"title": "Tool Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4244,15 +4286,6 @@
"description": "Detach a tool from an agent.",
"operationId": "detach_tool",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "tool_id",
"in": "path",
@@ -4261,6 +4294,21 @@
"type": "string",
"title": "Tool Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4294,15 +4342,6 @@
"description": "Attach a tool to an agent.",
"operationId": "modify_approval",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "tool_name",
"in": "path",
@@ -4312,6 +4351,21 @@
"title": "Tool Name"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "requires_approval",
"in": "query",
@@ -4353,15 +4407,6 @@
"description": "Attach a source to an agent.",
"operationId": "attach_source_to_agent",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "source_id",
"in": "path",
@@ -4370,6 +4415,21 @@
"type": "string",
"title": "Source Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4403,15 +4463,6 @@
"description": "Attach a folder to an agent.",
"operationId": "attach_folder_to_agent",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "folder_id",
"in": "path",
@@ -4420,6 +4471,21 @@
"type": "string",
"title": "Folder Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4453,15 +4519,6 @@
"description": "Detach a source from an agent.",
"operationId": "detach_source_from_agent",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "source_id",
"in": "path",
@@ -4470,6 +4527,21 @@
"type": "string",
"title": "Source Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4503,15 +4575,6 @@
"description": "Detach a folder from an agent.",
"operationId": "detach_folder_from_agent",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "folder_id",
"in": "path",
@@ -4520,6 +4583,21 @@
"type": "string",
"title": "Folder Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4559,8 +4637,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4598,15 +4682,6 @@
"description": "Opens a specific file for a given agent.\n\nThis endpoint marks a specific file as open in the agent's file state.\nThe file will be included in the agent's working memory view.\nReturns a list of file names that were closed due to LRU eviction.",
"operationId": "open_file",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "file_id",
"in": "path",
@@ -4615,6 +4690,21 @@
"type": "string",
"title": "File Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4652,15 +4742,6 @@
"description": "Closes a specific file for a given agent.\n\nThis endpoint marks a specific file as closed in the agent's file state.\nThe file will be removed from the agent's working memory view.",
"operationId": "close_file",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "file_id",
"in": "path",
@@ -4669,6 +4750,21 @@
"type": "string",
"title": "File Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -4706,8 +4802,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "before",
@@ -4832,8 +4934,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "before",
@@ -4958,8 +5066,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "before",
@@ -5118,8 +5232,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -5153,15 +5273,6 @@
"description": "Retrieve a core memory block from an agent.",
"operationId": "retrieve_core_memory_block",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "block_label",
"in": "path",
@@ -5170,6 +5281,21 @@
"type": "string",
"title": "Block Label"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -5201,15 +5327,6 @@
"description": "Updates a core memory block of an agent.",
"operationId": "modify_core_memory_block",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "block_label",
"in": "path",
@@ -5218,6 +5335,21 @@
"type": "string",
"title": "Block Label"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -5267,8 +5399,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "before",
@@ -5387,15 +5525,6 @@
"description": "Attach a core memory block to an agent.",
"operationId": "attach_core_memory_block",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "block_id",
"in": "path",
@@ -5404,6 +5533,21 @@
"type": "string",
"title": "Block Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -5437,15 +5581,6 @@
"description": "Detach a core memory block from an agent.",
"operationId": "detach_core_memory_block",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "block_id",
"in": "path",
@@ -5454,6 +5589,21 @@
"type": "string",
"title": "Block Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -5493,8 +5643,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "after",
@@ -5627,8 +5783,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -5682,8 +5844,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "query",
@@ -5818,15 +5986,6 @@
"description": "Delete a memory from an agent's archival memory store.",
"operationId": "delete_passage",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "memory_id",
"in": "path",
@@ -5835,6 +5994,21 @@
"type": "string",
"title": "Memory Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"responses": {
@@ -5872,8 +6046,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "before",
@@ -6068,8 +6248,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -6113,15 +6299,6 @@
"description": "Update the details of a message associated with an agent.",
"operationId": "modify_message",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
},
{
"name": "message_id",
"in": "path",
@@ -6130,6 +6307,21 @@
"type": "string",
"title": "Message Id"
}
},
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -6236,8 +6428,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -6288,8 +6486,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -6385,8 +6589,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -6436,8 +6646,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "add_default_initial_messages",
@@ -6489,8 +6705,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "manager_type",
@@ -6633,8 +6855,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
}
],
"requestBody": {
@@ -6694,8 +6922,14 @@
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
}
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "max_message_length",

View File

@@ -33,8 +33,6 @@ LETTA_TOOL_MODULE_NAMES = [
DEFAULT_ORG_ID = "org-00000000-0000-4000-8000-000000000000"
DEFAULT_ORG_NAME = "default_org"
AGENT_ID_PATTERN = re.compile(r"^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.IGNORECASE)
# String in the error message for when the context window is too large
# Example full message:
# This model's maximum context length is 8192 tokens. However, your messages resulted in 8198 tokens (7450 in the messages, 748 in the functions). Please reduce the length of the messages or functions.

View File

@@ -1,16 +0,0 @@
import uuid
def is_valid_agent_id(agent_id: str) -> bool:
"""Check if string matches the pattern 'agent-{uuid}'"""
if not agent_id or not agent_id.startswith("agent-"):
return False
uuid_section = agent_id[6:]
try:
uuid.UUID(uuid_section)
return True
except ValueError:
return False

View File

@@ -11,6 +11,7 @@ from typing import Optional
import uvicorn
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from marshmallow import ValidationError
from sqlalchemy.exc import IntegrityError, OperationalError
@@ -46,6 +47,7 @@ from letta.helpers.pinecone_utils import get_pinecone_indices, should_use_pineco
from letta.jobs.scheduler import start_scheduler_with_leader_election
from letta.log import get_logger
from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
from letta.otel.tracing import get_trace_id
from letta.schemas.letta_message import create_letta_message_union_schema, create_letta_ping_schema
from letta.schemas.letta_message_content import (
create_letta_assistant_message_content_union_schema,
@@ -66,6 +68,7 @@ from letta.server.rest_api.static_files import mount_static_files
from letta.server.rest_api.utils import SENTRY_ENABLED
from letta.server.server import SyncServer
from letta.settings import settings, telemetry_settings
from letta.validators import PATH_VALIDATORS, PRIMITIVE_ID_PATTERNS
if SENTRY_ENABLED:
import sentry_sdk
@@ -230,6 +233,65 @@ def create_application() -> "FastAPI":
},
)
# Reasoning for this handler is the default path validation logic returns a pretty gnarly error message
# because of the uuid4 pattern. This handler rewrites the error message to be more user-friendly and less intimidating.
@app.exception_handler(RequestValidationError)
async def custom_request_validation_handler(request: Request, exc: RequestValidationError):
"""Generalize path `_id` validation messages and include example IDs.
- Rewrites string pattern/length mismatches to "primitive-{uuid4}"
- Preserves stringified `detail` and includes `trace_id`
- Adds top-level `examples` from `PATH_VALIDATORS` for offending params
"""
errors = exc.errors()
examples_set: set[str] = set()
content = {"trace_id": get_trace_id() or ""}
for err in errors:
fastapi_error_loc = err.get("loc", [])
# only rewrite path param validation errors (should expand in future)
if len(fastapi_error_loc) != 2 or fastapi_error_loc[0] != "path":
continue
# re-write the error message
parameter_name = fastapi_error_loc[1]
err_type = err.get("type")
if (
err_type in {"string_pattern_mismatch", "string_too_short", "string_too_long"}
and isinstance(parameter_name, str)
and parameter_name.endswith("_id")
):
primitive = parameter_name[:-3]
validator = PATH_VALIDATORS.get(primitive)
if validator:
# simplify default error message
err["msg"] = f"String should match pattern '{primitive}-{{uuid4}}'"
# rewrite as string_pattern_mismatch even if the input length is too short or too long (more intuitive for user)
if err_type in {"string_too_short", "string_too_long"}:
# FYI: the pattern is the same as the pattern inthe validator object but for some reason the validator object
# doesn't let you access it directly (unless you call into pydantic layer)
err["ctx"] = {"pattern": PRIMITIVE_ID_PATTERNS[primitive].pattern}
err["type"] = "string_pattern_mismatch"
# collect examples for top-level examples field (prevents duplicates and allows for multiple examples for multiple primitives)
# e.g. if there are 2 malformed agent ids, the examples field will contain 2 examples for the agent primitive
# e.g. if there is a malformed agent id and malformed folder id, the examples field will contain both examples, for both the agent and folder primitives
try:
exs = getattr(validator, "examples", None)
if exs:
for ex in exs:
examples_set.add(ex)
else:
examples_set.add(f"{primitive}-123e4567-e89b-42d3-8456-426614174000")
except Exception:
examples_set.add(f"{primitive}-123e4567-e89b-42d3-8456-426614174000")
# Preserve current API contract: stringified list of errors
content["detail"] = repr(errors)
if examples_set:
content["examples"] = sorted(examples_set)
return JSONResponse(status_code=422, content=content)
async def error_handler_with_code(request: Request, exc: Exception, code: int, detail: str | None = None):
logger.error(f"{type(exc).__name__}", exc_info=exc)
@@ -448,6 +510,9 @@ def create_application() -> "FastAPI":
logger.warning(f"Failed to setup SQLAlchemy instrumentation: {e}")
# Don't fail startup if instrumentation fails
# Ensure our validation handler overrides tracing's handler when tracing is enabled
app.add_exception_handler(RequestValidationError, custom_request_validation_handler)
for route in v1_routes:
app.include_router(route, prefix=API_PREFIX)
# this gives undocumented routes for "latest" and bare api calls.

View File

@@ -4,7 +4,7 @@ import traceback
from datetime import datetime, timezone
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Query, Request, UploadFile, status
from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Path, Query, Request, UploadFile, status
from fastapi.responses import JSONResponse
from marshmallow import ValidationError
from orjson import orjson
@@ -14,7 +14,7 @@ from starlette.responses import Response, StreamingResponse
from letta.agents.agent_loop import AgentLoop
from letta.agents.letta_agent_v2 import LettaAgentV2
from letta.constants import AGENT_ID_PATTERN, DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
from letta.constants import DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
from letta.data_sources.redis_client import get_redis_client
from letta.errors import (
AgentExportIdMappingError,
@@ -59,6 +59,7 @@ from letta.services.lettuce import LettuceClient
from letta.services.run_manager import RunManager
from letta.settings import settings
from letta.utils import safe_create_shielded_task, safe_create_task, truncate_file_visible_content
from letta.validators import PATH_VALIDATORS
# These can be forward refs, but because Fastapi needs them at runtime the must be imported normally
@@ -169,7 +170,7 @@ class IndentedORJSONResponse(Response):
@router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent")
async def export_agent(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
max_steps: int = 100,
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -352,7 +353,7 @@ async def import_agent(
@router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="retrieve_agent_context_window")
async def retrieve_agent_context_window(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -392,7 +393,7 @@ async def create_agent(
@router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
async def modify_agent(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
update_agent: UpdateAgent = Body(...),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -404,7 +405,7 @@ async def modify_agent(
@router.get("/{agent_id}/tools", response_model=list[Tool], operation_id="list_agent_tools")
async def list_agent_tools(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
before: Optional[str] = Query(
@@ -433,8 +434,8 @@ async def list_agent_tools(
@router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=AgentState, operation_id="attach_tool")
async def attach_tool(
agent_id: str,
tool_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -449,8 +450,8 @@ async def attach_tool(
@router.patch("/{agent_id}/tools/detach/{tool_id}", response_model=AgentState, operation_id="detach_tool")
async def detach_tool(
agent_id: str,
tool_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -465,9 +466,9 @@ async def detach_tool(
@router.patch("/{agent_id}/tools/approval/{tool_name}", response_model=AgentState, operation_id="modify_approval")
async def modify_approval(
agent_id: str,
tool_name: str,
requires_approval: bool,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -484,8 +485,8 @@ async def modify_approval(
@router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent")
async def attach_source(
agent_id: str,
source_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -511,8 +512,8 @@ async def attach_source(
@router.patch("/{agent_id}/folders/attach/{folder_id}", response_model=AgentState, operation_id="attach_folder_to_agent")
async def attach_folder_to_agent(
agent_id: str,
folder_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -538,8 +539,8 @@ async def attach_folder_to_agent(
@router.patch("/{agent_id}/sources/detach/{source_id}", response_model=AgentState, operation_id="detach_source_from_agent")
async def detach_source(
agent_id: str,
source_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -568,8 +569,8 @@ async def detach_source(
@router.patch("/{agent_id}/folders/detach/{folder_id}", response_model=AgentState, operation_id="detach_folder_from_agent")
async def detach_folder_from_agent(
agent_id: str,
folder_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -598,7 +599,7 @@ async def detach_folder_from_agent(
@router.patch("/{agent_id}/files/close-all", response_model=List[str], operation_id="close_all_open_files")
async def close_all_open_files(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -615,8 +616,8 @@ async def close_all_open_files(
@router.patch("/{agent_id}/files/{file_id}/open", response_model=List[str], operation_id="open_file")
async def open_file(
agent_id: str,
file_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -664,8 +665,8 @@ async def open_file(
@router.patch("/{agent_id}/files/{file_id}/close", response_model=None, operation_id="close_file")
async def close_file(
agent_id: str,
file_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -689,7 +690,7 @@ async def close_file(
@router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
async def retrieve_agent(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
include_relationships: list[str] | None = Query(
None,
description=(
@@ -704,9 +705,6 @@ async def retrieve_agent(
"""
Get the state of the agent.
"""
# Check if agent_id matches uuid4 format
if not AGENT_ID_PATTERN.match(agent_id):
raise HTTPException(status_code=400, detail=f"agent_id {agent_id} is not in the valid format 'agent-<uuid4>'")
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
@@ -715,7 +713,7 @@ async def retrieve_agent(
@router.delete("/{agent_id}", response_model=None, operation_id="delete_agent")
async def delete_agent(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -729,7 +727,7 @@ async def delete_agent(
@router.get("/{agent_id}/sources", response_model=list[Source], operation_id="list_agent_sources")
async def list_agent_sources(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
before: Optional[str] = Query(
@@ -760,7 +758,7 @@ async def list_agent_sources(
@router.get("/{agent_id}/folders", response_model=list[Source], operation_id="list_agent_folders")
async def list_agent_folders(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
before: Optional[str] = Query(
@@ -791,7 +789,7 @@ async def list_agent_folders(
@router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_agent_files")
async def list_agent_files(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
before: Optional[str] = Query(
None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order"
),
@@ -856,7 +854,7 @@ async def list_agent_files(
# TODO: remove? can also get with agent blocks
@router.get("/{agent_id}/core-memory", response_model=Memory, operation_id="retrieve_agent_memory")
async def retrieve_agent_memory(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -871,8 +869,8 @@ async def retrieve_agent_memory(
@router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="retrieve_core_memory_block")
async def retrieve_block(
agent_id: str,
block_label: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -886,7 +884,7 @@ async def retrieve_block(
@router.get("/{agent_id}/core-memory/blocks", response_model=list[Block], operation_id="list_core_memory_blocks")
async def list_blocks(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
before: Optional[str] = Query(
@@ -918,8 +916,8 @@ async def list_blocks(
@router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block")
async def modify_block(
agent_id: str,
block_label: str,
agent_id: str = PATH_VALIDATORS["agent"],
block_update: BlockUpdate = Body(...),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -941,8 +939,8 @@ async def modify_block(
@router.patch("/{agent_id}/core-memory/blocks/attach/{block_id}", response_model=AgentState, operation_id="attach_core_memory_block")
async def attach_block(
agent_id: str,
block_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -955,8 +953,8 @@ async def attach_block(
@router.patch("/{agent_id}/core-memory/blocks/detach/{block_id}", response_model=AgentState, operation_id="detach_core_memory_block")
async def detach_block(
agent_id: str,
block_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
@@ -969,7 +967,7 @@ async def detach_block(
@router.get("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="list_passages")
async def list_passages(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
after: str | None = Query(None, description="Unique ID of the memory to start the query range at."),
before: str | None = Query(None, description="Unique ID of the memory to end the query range at."),
@@ -998,7 +996,7 @@ async def list_passages(
@router.post("/{agent_id}/archival-memory", response_model=list[Passage], operation_id="create_passage")
async def create_passage(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
request: CreateArchivalMemory = Body(...),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1015,7 +1013,7 @@ async def create_passage(
@router.get("/{agent_id}/archival-memory/search", response_model=ArchivalMemorySearchResponse, operation_id="search_archival_memory")
async def search_archival_memory(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
query: str = Query(..., description="String to search for using semantic similarity"),
tags: Optional[List[str]] = Query(None, description="Optional list of tags to filter search results"),
tag_match_mode: Literal["any", "all"] = Query(
@@ -1062,8 +1060,8 @@ async def search_archival_memory(
# @router.delete("/{agent_id}/archival")
@router.delete("/{agent_id}/archival-memory/{memory_id}", response_model=None, operation_id="delete_passage")
async def delete_passage(
agent_id: str,
memory_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
# memory_id: str = Query(..., description="Unique ID of the memory to be deleted."),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1084,7 +1082,7 @@ AgentMessagesResponse = Annotated[
@router.get("/{agent_id}/messages", response_model=AgentMessagesResponse, operation_id="list_messages")
async def list_messages(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: "SyncServer" = Depends(get_letta_server),
before: Optional[str] = Query(
None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
@@ -1129,8 +1127,8 @@ async def list_messages(
@router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_message")
async def modify_message(
agent_id: str,
message_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
request: LettaMessageUpdateUnion = Body(...),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1152,8 +1150,8 @@ async def modify_message(
operation_id="send_message",
)
async def send_message(
agent_id: str,
request_obj: Request, # FastAPI Request
agent_id: str = PATH_VALIDATORS["agent"],
server: SyncServer = Depends(get_letta_server),
request: LettaRequest = Body(...),
headers: HeaderParams = Depends(get_headers),
@@ -1279,8 +1277,8 @@ async def send_message(
},
)
async def send_message_streaming(
agent_id: str,
request_obj: Request, # FastAPI Request
agent_id: str = PATH_VALIDATORS["agent"],
server: SyncServer = Depends(get_letta_server),
request: LettaStreamingRequest = Body(...),
headers: HeaderParams = Depends(get_headers),
@@ -1313,7 +1311,7 @@ class CancelAgentRunRequest(BaseModel):
@router.post("/{agent_id}/messages/cancel", operation_id="cancel_agent_run")
async def cancel_agent_run(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
request: CancelAgentRunRequest = Body(None),
server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1394,11 +1392,11 @@ async def _process_message_background(
run_id: str,
server: SyncServer,
actor: User,
agent_id: str,
messages: list[MessageCreate],
use_assistant_message: bool,
assistant_message_tool_name: str,
assistant_message_tool_kwarg: str,
agent_id: str = PATH_VALIDATORS["agent"],
max_steps: int = DEFAULT_MAX_STEPS,
include_return_message_types: list[MessageType] | None = None,
) -> None:
@@ -1489,7 +1487,7 @@ async def _process_message_background(
operation_id="create_agent_message_async",
)
async def send_message_async(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
server: SyncServer = Depends(get_letta_server),
request: LettaAsyncRequest = Body(...),
headers: HeaderParams = Depends(get_headers),
@@ -1593,7 +1591,7 @@ async def send_message_async(
@router.patch("/{agent_id}/reset-messages", response_model=AgentState, operation_id="reset_messages")
async def reset_messages(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
add_default_initial_messages: bool = Query(default=False, description="If true, adds the default initial messages after resetting."),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1607,7 +1605,7 @@ async def reset_messages(
@router.get("/{agent_id}/groups", response_model=list[Group], operation_id="list_agent_groups")
async def list_agent_groups(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
manager_type: str | None = Query(None, description="Manager type to filter groups by"),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1643,7 +1641,7 @@ async def list_agent_groups(
operation_id="preview_raw_payload",
)
async def preview_raw_payload(
agent_id: str,
agent_id: str = PATH_VALIDATORS["agent"],
request: Union[LettaRequest, LettaStreamingRequest] = Body(...),
server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
@@ -1688,8 +1686,8 @@ async def preview_raw_payload(
@router.post("/{agent_id}/summarize", status_code=204, operation_id="summarize_agent_conversation")
async def summarize_agent_conversation(
agent_id: str,
request_obj: Request, # FastAPI Request
agent_id: str = PATH_VALIDATORS["agent"],
max_message_length: int = Query(..., description="Maximum number of messages to retain after summarization."),
server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),

View File

@@ -25,9 +25,9 @@ from letta.constants import (
INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES,
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
)
from letta.errors import LettaAgentNotFoundError
from letta.helpers import ToolRulesSolver
from letta.helpers.datetime_helpers import get_utc_time
from letta.helpers.validators import is_valid_agent_id
from letta.llm_api.llm_client import LLMClient
from letta.log import get_logger
from letta.orm import (
@@ -110,6 +110,7 @@ from letta.services.source_manager import SourceManager
from letta.services.tool_manager import ToolManager
from letta.settings import DatabaseChoice, model_settings, settings
from letta.utils import calculate_file_defaults_based_on_context_window, enforce_types, united_diff
from letta.validators import is_valid_id
logger = get_logger(__name__)
@@ -982,8 +983,10 @@ class AgentManager:
include_relationships: Optional[List[str]] = None,
) -> PydanticAgentState:
"""Fetch an agent by its ID."""
if not is_valid_agent_id(agent_id):
raise NoResultFound("Agent id should match agent-{uuid}")
# Check if agent_id matches uuid4 format
if not is_valid_id("agent", agent_id):
raise LettaAgentNotFoundError(f"agent_id {agent_id} is not in the valid format 'agent-<uuid4>'")
async with db_registry.async_session() as session:
try:

27
letta/validators.py Normal file
View File

@@ -0,0 +1,27 @@
import re
from fastapi import Path
# TODO: extract this list from routers/v1/__init__.py and ROUTERS
primitives = ["agent", "message", "run", "job", "group", "block", "file", "folder", "source", "tool", "mcp_server"]
PRIMITIVE_ID_PATTERNS = {
# f-string interpolation gets confused because of the regex's required curly braces {}
primitive: re.compile("^" + primitive + "-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
for primitive in primitives
}
PATH_VALIDATORS = {}
for primitive in primitives:
PATH_VALIDATORS[primitive] = Path(
description=f"The ID of the {primitive} in the format '{primitive}-<uuid4>'",
pattern=PRIMITIVE_ID_PATTERNS[primitive].pattern,
examples=[f"{primitive}-123e4567-e89b-42d3-8456-426614174000"],
# len(agent) + len("-") + len(uuid4)
min_length=len(primitive) + 1 + 36,
max_length=len(primitive) + 1 + 36,
)
def is_valid_id(primitive: str, id: str) -> bool:
return PRIMITIVE_ID_PATTERNS[primitive].match(id) is not None