From 85ed29274cd1c97e1ff40a4bcc45ba70f76f797c Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Fri, 24 Oct 2025 16:03:15 -0700 Subject: [PATCH] feat: clean up block return object [LET-5784] (#5641) * fix: fix deep research agent * chore: update blocks response * add message * update agents * update * use blockresponse * undo merge conflict * add internal agents and blocks * remove unnecessary internal agent route * fix utils server test --------- Co-authored-by: christinatong01 --- fern/assets/leaderboard.js | 153 +++ fern/openapi-overrides.yml | 14 + fern/openapi.json | 966 +++++++++++++++++- letta/schemas/block.py | 21 +- letta/server/rest_api/routers/v1/__init__.py | 4 + letta/server/rest_api/routers/v1/agents.py | 8 +- letta/server/rest_api/routers/v1/blocks.py | 14 +- .../server/rest_api/routers/v1/identities.py | 4 +- .../rest_api/routers/v1/internal_agents.py | 31 + .../rest_api/routers/v1/internal_blocks.py | 177 ++++ 10 files changed, 1349 insertions(+), 43 deletions(-) create mode 100644 fern/assets/leaderboard.js create mode 100644 letta/server/rest_api/routers/v1/internal_agents.py create mode 100644 letta/server/rest_api/routers/v1/internal_blocks.py diff --git a/fern/assets/leaderboard.js b/fern/assets/leaderboard.js new file mode 100644 index 00000000..f627ce6c --- /dev/null +++ b/fern/assets/leaderboard.js @@ -0,0 +1,153 @@ +/* ────────────────────────────────────────────────────────── + assets/leaderboard.js + Load via docs.yml → js: - path: assets/leaderboard.js + (strategy: lazyOnload is fine) + ────────────────────────────────────────────────────────── */ + +import yaml from 'https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/+esm'; + +console.log('🏁 leaderboard.js loaded on', location.pathname); + +const COST_CAP = 120; + +/* ---------- helpers ---------- */ +const pct = (v) => Number(v).toPrecision(3) + '%'; +const cost = (v) => '$' + Number(v).toFixed(2); +const ready = (cb) => + document.readyState === 'loading' + ? document.addEventListener('DOMContentLoaded', cb) + : cb(); + +/* ---------- main ---------- */ +ready(async () => { + // const host = document.getElementById('letta-leaderboard'); + // if (!host) { + // console.warn('LB-script: #letta-leaderboard not found - bailing out.'); + // return; + // } + /* ---- wait for the leaderboard container to appear (SPA nav safe) ---- */ + const host = await new Promise((resolve, reject) => { + const el = document.getElementById('letta-leaderboard'); + if (el) return resolve(el); // SSR / hard refresh path + + const obs = new MutationObserver(() => { + const found = document.getElementById('letta-leaderboard'); + if (found) { + obs.disconnect(); + resolve(found); // CSR navigation path + } + }); + obs.observe(document.body, { childList: true, subtree: true }); + + setTimeout(() => { + obs.disconnect(); + reject(new Error('#letta-leaderboard never appeared')); + }, 5000); // safety timeout + }).catch((err) => { + console.warn('LB-script:', err.message); + return null; + }); + if (!host) return; // still no luck → give up + + /* ----- figure out URL of data.yaml ----- */ + // const path = location.pathname.endsWith('/') + // ? location.pathname + // : location.pathname.replace(/[^/]*$/, ''); // strip file/slug + // const dataUrl = `${location.origin}${path}data.yaml`; + // const dataUrl = `${location.origin}/leaderboard/data.yaml`; // one-liner, always right + // const dataUrl = `${location.origin}/assets/leaderboard.yaml`; + // const dataUrl = `./assets/leaderboard.yaml`; // one-liner, always right + // const dataUrl = `${location.origin}/data.yaml`; // one-liner, always right + const dataUrl = + 'https://raw.githubusercontent.com/letta-ai/letta-evals/refs/heads/main/letta-leaderboard/leaderboard_results.yaml'; + // const dataUrl = 'https://cdn.jsdelivr.net/gh/letta-ai/letta-evals@latest/letta-leaderboard/leaderboard_results.yaml'; + + console.log('LB-script: fetching', dataUrl); + + /* ----- fetch & parse YAML ----- */ + let rows; + try { + const resp = await fetch(dataUrl); + console.log(`LB-script: status ${resp.status}`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + rows = yaml.load(await resp.text()); + } catch (err) { + console.error('LB-script: failed to load YAML →', err); + return; + } + + /* ----- wire up table ----- */ + const dir = Object.create(null); + const tbody = document.getElementById('lb-body'); + const searchI = document.getElementById('lb-search'); + const headers = document.querySelectorAll('#lb-table thead th[data-key]'); + searchI.value = ''; // clear any persisted filter + + const render = () => { + const q = searchI.value.toLowerCase(); + tbody.innerHTML = rows + .map((r) => { + const over = r.total_cost > COST_CAP; + const barW = over ? '100%' : (r.total_cost / COST_CAP) * 100 + '%'; + const costCls = over ? 'cost-high' : 'cost-ok'; + const warnIcon = over + ? `` + : ''; + + return ` + + ${r.model} + + +
+ ${pct(r.average)} + + + +
+ ${cost(r.total_cost)} + ${warnIcon} + + `; + }) + .join(''); + }; + + const setIndicator = (activeKey) => { + headers.forEach((h) => { + h.classList.remove('asc', 'desc'); + if (h.dataset.key === activeKey) h.classList.add(dir[activeKey]); + }); + }; + + /* initial sort ↓ */ + dir.average = 'desc'; + rows.sort((a, b) => b.average - a.average); + setIndicator('average'); + render(); + + /* search */ + searchI.addEventListener('input', render); + + /* column sorting */ + headers.forEach((th) => { + const key = th.dataset.key; + th.addEventListener('click', () => { + const asc = dir[key] === 'desc'; + dir[key] = asc ? 'asc' : 'desc'; + + rows.sort((a, b) => { + const va = a[key], + vb = b[key]; + const cmp = + typeof va === 'number' + ? va - vb + : String(va).localeCompare(String(vb)); + return asc ? cmp : -cmp; + }); + + setIndicator(key); + render(); + }); + }); +}); diff --git a/fern/openapi-overrides.yml b/fern/openapi-overrides.yml index be730bef..49fc2592 100644 --- a/fern/openapi-overrides.yml +++ b/fern/openapi-overrides.yml @@ -1258,6 +1258,20 @@ paths: /v1/_internal_runs/: get: x-fern-ignore: true + /v1/_internal_blocks/: + get: + x-fern-ignore: true + post: + x-fern-ignore: true + /v1/_internal_blocks/{block_id}: + delete: + x-fern-ignore: true + /v1/_internal_blocks/{block_id}/agents: + get: + x-fern-ignore: true + /v1/_internal_agents/{agent_id}/core-memory/blocks/{block_label}: + patch: + x-fern-ignore: true /v1/projects: get: x-fern-sdk-group-name: diff --git a/fern/openapi.json b/fern/openapi.json index 6675a63e..5b882d00 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -5900,7 +5900,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -5964,7 +5964,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -6094,7 +6094,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" }, "title": "Response List Core Memory Blocks" } @@ -9494,7 +9494,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" }, "title": "Response List Blocks For Identity" } @@ -9514,6 +9514,654 @@ } } }, + "/v1/_internal_agents/{agent_id}/core-memory/blocks/{block_label}": { + "patch": { + "tags": ["_internal_agents"], + "summary": "Modify Block For Agent", + "description": "Updates a core memory block of an agent.", + "operationId": "modify_internal_core_memory_block", + "parameters": [ + { + "name": "block_label", + "in": "path", + "required": true, + "schema": { + "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-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_blocks/": { + "get": { + "tags": ["_internal_blocks"], + "summary": "List Blocks", + "operationId": "list_internal_blocks", + "parameters": [ + { + "name": "label", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Labels to include (e.g. human, persona)", + "title": "Label" + }, + "description": "Labels to include (e.g. human, persona)" + }, + { + "name": "templates_only", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include only templates", + "default": false, + "title": "Templates Only" + }, + "description": "Whether to include only templates" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Name of the block", + "title": "Name" + }, + "description": "Name of the block" + }, + { + "name": "identity_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by identifier id", + "title": "Identity Id" + }, + "description": "Search agents by identifier id" + }, + { + "name": "identifier_keys", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Search agents by identifier keys", + "title": "Identifier Keys" + }, + "description": "Search agents by identifier keys" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by project id", + "title": "Project Id" + }, + "description": "Search blocks by project id" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Number of blocks to return", + "default": 50, + "title": "Limit" + }, + "description": "Number of blocks to return" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + "title": "Before" + }, + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + "title": "After" + }, + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "label_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels.", + "title": "Label Search" + }, + "description": "Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels." + }, + { + "name": "description_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by description. If provided, returns blocks that match this description. This is a full-text search on block descriptions.", + "title": "Description Search" + }, + "description": "Search blocks by description. If provided, returns blocks that match this description. This is a full-text search on block descriptions." + }, + { + "name": "value_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by value. If provided, returns blocks that match this value.", + "title": "Value Search" + }, + "description": "Search blocks by value. If provided, returns blocks that match this value." + }, + { + "name": "connected_to_agents_count_gt", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have more than this number of connected agents.", + "title": "Connected To Agents Count Gt" + }, + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have more than this number of connected agents." + }, + { + "name": "connected_to_agents_count_lt", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have less than this number of connected agents.", + "title": "Connected To Agents Count Lt" + }, + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have less than this number of connected agents." + }, + { + "name": "connected_to_agents_count_eq", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the exact number of connected agents. If provided, returns blocks that have exactly this number of connected agents.", + "title": "Connected To Agents Count Eq" + }, + "description": "Filter blocks by the exact number of connected agents. If provided, returns blocks that have exactly this number of connected agents." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Block" + }, + "title": "Response List Internal Blocks" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["_internal_blocks"], + "summary": "Create Block", + "operationId": "create_internal_block", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBlock" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_blocks/{block_id}": { + "delete": { + "tags": ["_internal_blocks"], + "summary": "Delete Block", + "operationId": "delete_internal_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[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 block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_blocks/{block_id}/agents": { + "get": { + "tags": ["_internal_blocks"], + "summary": "List Agents For Block", + "description": "Retrieves all agents associated with the specified block.\nRaises a 404 if the block does not exist.", + "operationId": "list_agents_for_internal_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[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 block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + "title": "Before" + }, + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + "title": "After" + }, + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of agents to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of agents to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "include_relationships", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "title": "Include Relationships" + }, + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions." + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + }, + "title": "Response List Agents For Internal Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/v1/_internal_runs/": { "get": { "tags": ["_internal_runs"], @@ -11045,7 +11693,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" }, "title": "Response List Blocks" } @@ -11085,7 +11733,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -11173,7 +11821,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -11259,7 +11907,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -11492,7 +12140,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -11548,7 +12196,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Block" + "$ref": "#/components/schemas/BlockResponse" } } } @@ -19142,7 +19790,7 @@ "title": "Project Id", "description": "The associated project id." }, - "name": { + "template_name": { "anyOf": [ { "type": "string" @@ -19151,8 +19799,8 @@ "type": "null" } ], - "title": "Name", - "description": "The id of the template." + "title": "Template Name", + "description": "Name of the block if it is a template." }, "is_template": { "type": "boolean", @@ -19160,6 +19808,18 @@ "description": "Whether the block is a template (e.g. saved human/persona options).", "default": false }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, "base_template_id": { "anyOf": [ { @@ -19302,6 +19962,210 @@ "title": "Block", "description": "A Block represents a reserved section of the LLM's context window which is editable. `Block` objects contained in the `Memory` object, which is able to edit the Block values.\n\nParameters:\n label (str): The label of the block (e.g. 'human', 'persona'). This defines a category for the block.\n value (str): The value of the block. This is the string that is represented in the context window.\n limit (int): The character limit of the block.\n is_template (bool): Whether the block is a template (e.g. saved human/persona options). Non-template blocks are not stored in the database and are ephemeral, while templated blocks are stored in the database.\n label (str): The label of the block (e.g. 'human', 'persona'). This defines a category for the block.\n template_name (str): The name of the block template (if it is a template).\n description (str): Description of the block.\n metadata (Dict): Metadata of the block.\n user_id (str): The unique identifier of the user associated with the block." }, + "BlockResponse": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 20000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "(Deprecated) The name of the block template (if it is a template).", + "deprecated": true + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "description": "Whether the block is a template (e.g. saved human/persona options).", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "(Deprecated) The id of the template.", + "deprecated": true + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "(Deprecated) The base template id of the block.", + "deprecated": true + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "(Deprecated) The id of the deployment.", + "deprecated": true + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "(Deprecated) The id of the entity within the template.", + "deprecated": true + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "(Deprecated) Preserve the block on template migration.", + "default": false, + "deprecated": true + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "Label of the block (e.g. 'human', 'persona') in the context window." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "(Deprecated) Whether the agent has read-only access to the block.", + "default": false, + "deprecated": true + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "(Deprecated) If set to True, the block will be hidden.", + "deprecated": true + }, + "id": { + "type": "string", + "pattern": "^block-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Block", + "examples": ["block-123e4567-e89b-12d3-a456-426614174000"] + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Block." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that last updated this Block." + } + }, + "type": "object", + "required": ["value"], + "title": "BlockResponse" + }, "BlockSchema": { "properties": { "value": { @@ -19327,7 +20191,7 @@ "title": "Project Id", "description": "The associated project id." }, - "name": { + "template_name": { "anyOf": [ { "type": "string" @@ -19336,14 +20200,26 @@ "type": "null" } ], - "title": "Name", - "description": "The id of the template." + "title": "Template Name", + "description": "Name of the block if it is a template." }, "is_template": { "type": "boolean", "title": "Is Template", "default": false }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, "base_template_id": { "anyOf": [ { @@ -19491,7 +20367,7 @@ "title": "Project Id", "description": "The associated project id." }, - "name": { + "template_name": { "anyOf": [ { "type": "string" @@ -19500,8 +20376,8 @@ "type": "null" } ], - "title": "Name", - "description": "The id of the template." + "title": "Template Name", + "description": "Name of the block if it is a template." }, "is_template": { "type": "boolean", @@ -19509,6 +20385,18 @@ "description": "Whether the block is a template (e.g. saved human/persona options).", "default": false }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, "base_template_id": { "anyOf": [ { @@ -21858,7 +22746,7 @@ "title": "Project Id", "description": "The associated project id." }, - "name": { + "template_name": { "anyOf": [ { "type": "string" @@ -21867,14 +22755,26 @@ "type": "null" } ], - "title": "Name", - "description": "The id of the template." + "title": "Template Name", + "description": "Name of the block if it is a template." }, "is_template": { "type": "boolean", "title": "Is Template", "default": false }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, "base_template_id": { "anyOf": [ { @@ -22672,7 +23572,7 @@ "title": "Project Id", "description": "The associated project id." }, - "name": { + "template_name": { "anyOf": [ { "type": "string" @@ -22681,8 +23581,8 @@ "type": "null" } ], - "title": "Name", - "description": "The id of the template." + "title": "Template Name", + "description": "Name of the block if it is a template." }, "is_template": { "type": "boolean", @@ -22690,6 +23590,18 @@ "description": "Whether the block is a template (e.g. saved human/persona options).", "default": false }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, "base_template_id": { "anyOf": [ { @@ -25298,7 +26210,7 @@ "title": "Project Id", "description": "The associated project id." }, - "name": { + "template_name": { "anyOf": [ { "type": "string" @@ -25307,7 +26219,7 @@ "type": "null" } ], - "title": "Name", + "title": "Template Name", "description": "Name of the block if it is a template." }, "is_template": { diff --git a/letta/schemas/block.py b/letta/schemas/block.py index bac5ed02..32c28b3b 100644 --- a/letta/schemas/block.py +++ b/letta/schemas/block.py @@ -21,9 +21,9 @@ class BaseBlock(LettaBase, validate_assignment=True): project_id: Optional[str] = Field(None, description="The associated project id.") # template data (optional) - template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name") + template_name: Optional[str] = Field(None, description="Name of the block if it is a template.") is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).") - template_id: Optional[str] = Field(None, description="The id of the template.", alias="name") + template_id: Optional[str] = Field(None, description="The id of the template.") base_template_id: Optional[str] = Field(None, description="The base template id of the block.") deployment_id: Optional[str] = Field(None, description="The id of the deployment.") entity_id: Optional[str] = Field(None, description="The id of the entity within the template.") @@ -102,6 +102,21 @@ class Block(BaseBlock): last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.") +class BlockResponse(Block): + template_name: Optional[str] = Field( + None, description="(Deprecated) The name of the block template (if it is a template).", deprecated=True + ) + template_id: Optional[str] = Field(None, description="(Deprecated) The id of the template.", deprecated=True) + base_template_id: Optional[str] = Field(None, description="(Deprecated) The base template id of the block.", deprecated=True) + deployment_id: Optional[str] = Field(None, description="(Deprecated) The id of the deployment.", deprecated=True) + entity_id: Optional[str] = Field(None, description="(Deprecated) The id of the entity within the template.", deprecated=True) + preserve_on_migration: Optional[bool] = Field( + False, description="(Deprecated) Preserve the block on template migration.", deprecated=True + ) + read_only: bool = Field(False, description="(Deprecated) Whether the agent has read-only access to the block.", deprecated=True) + hidden: Optional[bool] = Field(None, description="(Deprecated) If set to True, the block will be hidden.", deprecated=True) + + class FileBlock(Block): file_id: str = Field(..., description="Unique identifier of the file.") source_id: str = Field(..., description="Unique identifier of the source.") @@ -149,7 +164,7 @@ class CreateBlock(BaseBlock): project_id: Optional[str] = Field(None, description="The associated project id.") # block templates is_template: bool = False - template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name") + template_name: Optional[str] = Field(None, description="Name of the block if it is a template.") @model_validator(mode="before") @classmethod diff --git a/letta/server/rest_api/routers/v1/__init__.py b/letta/server/rest_api/routers/v1/__init__.py index 520a77b4..1e3a1c9b 100644 --- a/letta/server/rest_api/routers/v1/__init__.py +++ b/letta/server/rest_api/routers/v1/__init__.py @@ -7,6 +7,8 @@ from letta.server.rest_api.routers.v1.folders import router as folders_router from letta.server.rest_api.routers.v1.groups import router as groups_router from letta.server.rest_api.routers.v1.health import router as health_router from letta.server.rest_api.routers.v1.identities import router as identities_router +from letta.server.rest_api.routers.v1.internal_agents import router as internal_agents_router +from letta.server.rest_api.routers.v1.internal_blocks import router as internal_blocks_router from letta.server.rest_api.routers.v1.internal_runs import router as internal_runs_router from letta.server.rest_api.routers.v1.internal_templates import router as internal_templates_router from letta.server.rest_api.routers.v1.jobs import router as jobs_router @@ -32,6 +34,8 @@ ROUTERS = [ chat_completions_router, groups_router, identities_router, + internal_agents_router, + internal_blocks_router, internal_runs_router, internal_templates_router, llm_router, diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index 8c601713..d77238c0 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -32,7 +32,7 @@ from letta.otel.context import get_ctx_attributes from letta.otel.metric_registry import MetricRegistry from letta.schemas.agent import AgentRelationships, AgentState, CreateAgent, UpdateAgent from letta.schemas.agent_file import AgentFileSchema -from letta.schemas.block import BaseBlock, Block, BlockUpdate +from letta.schemas.block import BaseBlock, Block, BlockResponse, BlockUpdate from letta.schemas.enums import AgentType, RunStatus from letta.schemas.file import AgentFileAttachment, FileMetadataBase, PaginatedAgentFiles from letta.schemas.group import Group @@ -915,7 +915,7 @@ async def retrieve_agent_memory( return await server.get_agent_memory_async(agent_id=agent_id, actor=actor) -@router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="retrieve_core_memory_block") +@router.get("/{agent_id}/core-memory/blocks/{block_label}", response_model=BlockResponse, operation_id="retrieve_core_memory_block") async def retrieve_block_for_agent( block_label: str, agent_id: AgentId, @@ -930,7 +930,7 @@ async def retrieve_block_for_agent( return await server.agent_manager.get_block_with_label_async(agent_id=agent_id, block_label=block_label, actor=actor) -@router.get("/{agent_id}/core-memory/blocks", response_model=list[Block], operation_id="list_core_memory_blocks") +@router.get("/{agent_id}/core-memory/blocks", response_model=list[BlockResponse], operation_id="list_core_memory_blocks") async def list_blocks_for_agent( agent_id: AgentId, server: "SyncServer" = Depends(get_letta_server), @@ -962,7 +962,7 @@ async def list_blocks_for_agent( ) -@router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block") +@router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=BlockResponse, operation_id="modify_core_memory_block") async def modify_block_for_agent( block_label: str, agent_id: AgentId, diff --git a/letta/server/rest_api/routers/v1/blocks.py b/letta/server/rest_api/routers/v1/blocks.py index 14f00984..390ea220 100644 --- a/letta/server/rest_api/routers/v1/blocks.py +++ b/letta/server/rest_api/routers/v1/blocks.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query from letta.orm.errors import NoResultFound from letta.schemas.agent import AgentRelationships, AgentState -from letta.schemas.block import BaseBlock, Block, BlockUpdate, CreateBlock +from letta.schemas.block import BaseBlock, Block, BlockResponse, BlockUpdate, CreateBlock from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer from letta.utils import is_1_0_sdk_version @@ -16,7 +16,7 @@ if TYPE_CHECKING: router = APIRouter(prefix="/blocks", tags=["blocks"]) -@router.get("/", response_model=List[Block], operation_id="list_blocks") +@router.get("/", response_model=List[BlockResponse], operation_id="list_blocks") async def list_blocks( # query parameters label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"), @@ -117,7 +117,7 @@ async def count_blocks( return await server.block_manager.size_async(actor=actor) -@router.post("/", response_model=Block, operation_id="create_block") +@router.post("/", response_model=BlockResponse, operation_id="create_block") async def create_block( create_block: CreateBlock = Body(...), server: SyncServer = Depends(get_letta_server), @@ -128,7 +128,7 @@ async def create_block( return await server.block_manager.create_or_update_block_async(actor=actor, block=block) -@router.patch("/{block_id}", response_model=Block, operation_id="modify_block") +@router.patch("/{block_id}", response_model=BlockResponse, operation_id="modify_block") async def modify_block( block_id: BlockId, block_update: BlockUpdate = Body(...), @@ -149,7 +149,7 @@ async def delete_block( await server.block_manager.delete_block_async(block_id=block_id, actor=actor) -@router.get("/{block_id}", response_model=Block, operation_id="retrieve_block") +@router.get("/{block_id}", response_model=BlockResponse, operation_id="retrieve_block") async def retrieve_block( block_id: BlockId, server: SyncServer = Depends(get_letta_server), @@ -214,7 +214,7 @@ async def list_agents_for_block( return agents -@router.patch("/{block_id}/identities/attach/{identity_id}", response_model=Block, operation_id="attach_identity_to_block") +@router.patch("/{block_id}/identities/attach/{identity_id}", response_model=BlockResponse, operation_id="attach_identity_to_block") async def attach_identity_to_block( identity_id: str, block_id: BlockId, @@ -233,7 +233,7 @@ async def attach_identity_to_block( return await server.block_manager.get_block_by_id_async(block_id=block_id, actor=actor) -@router.patch("/{block_id}/identities/detach/{identity_id}", response_model=Block, operation_id="detach_identity_from_block") +@router.patch("/{block_id}/identities/detach/{identity_id}", response_model=BlockResponse, operation_id="detach_identity_from_block") async def detach_identity_from_block( identity_id: str, block_id: BlockId, diff --git a/letta/server/rest_api/routers/v1/identities.py b/letta/server/rest_api/routers/v1/identities.py index 8dc7bdce..dd81e87e 100644 --- a/letta/server/rest_api/routers/v1/identities.py +++ b/letta/server/rest_api/routers/v1/identities.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Depends, Header, Query from letta.orm.errors import NoResultFound, UniqueConstraintViolationError from letta.schemas.agent import AgentRelationships, AgentState -from letta.schemas.block import Block +from letta.schemas.block import Block, BlockResponse from letta.schemas.identity import ( Identity, IdentityCreate, @@ -188,7 +188,7 @@ async def list_agents_for_identity( ) -@router.get("/{identity_id}/blocks", response_model=List[Block], operation_id="list_blocks_for_identity") +@router.get("/{identity_id}/blocks", response_model=List[BlockResponse], operation_id="list_blocks_for_identity") async def list_blocks_for_identity( identity_id: IdentityId, before: Optional[str] = Query( diff --git a/letta/server/rest_api/routers/v1/internal_agents.py b/letta/server/rest_api/routers/v1/internal_agents.py new file mode 100644 index 00000000..de067c27 --- /dev/null +++ b/letta/server/rest_api/routers/v1/internal_agents.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Body, Depends + +from letta.schemas.block import Block, BlockUpdate +from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server +from letta.server.server import SyncServer +from letta.validators import AgentId + +router = APIRouter(prefix="/_internal_agents", tags=["_internal_agents"]) + + +@router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_internal_core_memory_block") +async def modify_block_for_agent( + block_label: str, + agent_id: AgentId, + block_update: BlockUpdate = Body(...), + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Updates a core memory block of an agent. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + + block = await server.agent_manager.modify_block_by_label_async( + agent_id=agent_id, block_label=block_label, block_update=block_update, actor=actor + ) + + # This should also trigger a system prompt change in the agent + await server.agent_manager.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True, update_timestamp=False) + + return block diff --git a/letta/server/rest_api/routers/v1/internal_blocks.py b/letta/server/rest_api/routers/v1/internal_blocks.py new file mode 100644 index 00000000..d9f2b988 --- /dev/null +++ b/letta/server/rest_api/routers/v1/internal_blocks.py @@ -0,0 +1,177 @@ +from typing import TYPE_CHECKING, List, Literal, Optional + +from fastapi import APIRouter, Body, Depends, Query + +from letta.schemas.agent import AgentState +from letta.schemas.block import Block, CreateBlock +from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server +from letta.server.server import SyncServer +from letta.utils import is_1_0_sdk_version +from letta.validators import BlockId + +if TYPE_CHECKING: + pass + +router = APIRouter(prefix="/_internal_blocks", tags=["_internal_blocks"]) + + +@router.get("/", response_model=List[Block], operation_id="list_internal_blocks") +async def list_blocks( + # query parameters + label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"), + templates_only: bool = Query(False, description="Whether to include only templates"), + name: Optional[str] = Query(None, description="Name of the block"), + identity_id: Optional[str] = Query(None, description="Search agents by identifier id"), + identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"), + project_id: Optional[str] = Query(None, description="Search blocks by project id"), + limit: Optional[int] = Query(50, description="Number of blocks to return"), + before: Optional[str] = Query( + None, + description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + ), + after: Optional[str] = Query( + None, + description="Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + ), + order: Literal["asc", "desc"] = Query( + "asc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + ), + order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"), + label_search: Optional[str] = Query( + None, + description=("Search blocks by label. If provided, returns blocks that match this label. This is a full-text search on labels."), + ), + description_search: Optional[str] = Query( + None, + description=( + "Search blocks by description. If provided, returns blocks that match this description. " + "This is a full-text search on block descriptions." + ), + ), + value_search: Optional[str] = Query( + None, + description=("Search blocks by value. If provided, returns blocks that match this value."), + ), + connected_to_agents_count_gt: Optional[int] = Query( + None, + description=( + "Filter blocks by the number of connected agents. " + "If provided, returns blocks that have more than this number of connected agents." + ), + ), + connected_to_agents_count_lt: Optional[int] = Query( + None, + description=( + "Filter blocks by the number of connected agents. " + "If provided, returns blocks that have less than this number of connected agents." + ), + ), + connected_to_agents_count_eq: Optional[List[int]] = Query( + None, + description=( + "Filter blocks by the exact number of connected agents. " + "If provided, returns blocks that have exactly this number of connected agents." + ), + ), + show_hidden_blocks: bool | None = Query( + False, + include_in_schema=False, + description="If set to True, include blocks marked as hidden in the results.", + ), + server: SyncServer = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + return await server.block_manager.get_blocks_async( + actor=actor, + label=label, + is_template=templates_only, + value_search=value_search, + label_search=label_search, + description_search=description_search, + template_name=name, + identity_id=identity_id, + identifier_keys=identifier_keys, + project_id=project_id, + before=before, + connected_to_agents_count_gt=connected_to_agents_count_gt, + connected_to_agents_count_lt=connected_to_agents_count_lt, + connected_to_agents_count_eq=connected_to_agents_count_eq, + limit=limit, + after=after, + ascending=(order == "asc"), + show_hidden_blocks=show_hidden_blocks, + ) + + +@router.post("/", response_model=Block, operation_id="create_internal_block") +async def create_block( + create_block: CreateBlock = Body(...), + server: SyncServer = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + block = Block(**create_block.model_dump()) + return await server.block_manager.create_or_update_block_async(actor=actor, block=block) + + +@router.delete("/{block_id}", operation_id="delete_internal_block") +async def delete_block( + block_id: BlockId, + server: SyncServer = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + await server.block_manager.delete_block_async(block_id=block_id, actor=actor) + + +@router.get("/{block_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_internal_block") +async def list_agents_for_block( + block_id: BlockId, + before: Optional[str] = Query( + None, + description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + ), + after: Optional[str] = Query( + None, + description="Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + ), + limit: Optional[int] = Query(50, description="Maximum number of agents to return"), + order: Literal["asc", "desc"] = Query( + "desc", description="Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + ), + order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"), + include_relationships: list[str] | None = Query( + None, + description=( + "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. " + "If not provided, all relationships are loaded by default. " + "Using this can optimize performance by reducing unnecessary joins." + "This is a legacy parameter, and no longer supported after 1.0.0 SDK versions." + ), + ), + include: List[str] = Query( + [], + description=("Specify which relational fields to include in the response. No relationships are included by default."), + ), + server: SyncServer = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Retrieves all agents associated with the specified block. + Raises a 404 if the block does not exist. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + if include_relationships is None and is_1_0_sdk_version(headers): + include_relationships = [] # don't default include all if using new SDK version + agents = await server.block_manager.get_agents_for_block_async( + block_id=block_id, + before=before, + after=after, + limit=limit, + ascending=(order == "asc"), + include_relationships=include_relationships, + include=include, + actor=actor, + ) + return agents