feat: add skills support to agentfile (#9287)

This commit is contained in:
cthomas
2026-02-04 20:10:19 -08:00
committed by Caren Thomas
parent fdc32f6054
commit 530d33c254
4 changed files with 217 additions and 3 deletions

View File

@@ -4727,6 +4727,62 @@
}
}
}
},
"post": {
"tags": ["agents"],
"summary": "Export Agent With Skills",
"description": "Export the serialized JSON representation of an agent with optional skills.\n\nThis POST endpoint allows including skills in the export by providing them in the request body.\nSkills are resolved client-side and passed as SkillSchema objects containing the skill files.",
"operationId": "export_agent_with_skills",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/ExportAgentRequest"
},
{
"type": "null"
}
],
"title": "Request"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/agents/import": {
@@ -25279,6 +25335,14 @@
"title": "Mcp Servers",
"description": "List of MCP servers in this agent file"
},
"skills": {
"items": {
"$ref": "#/components/schemas/SkillSchema"
},
"type": "array",
"title": "Skills",
"description": "List of skills in this agent file"
},
"metadata": {
"additionalProperties": {
"type": "string"
@@ -32503,6 +32567,33 @@
"title": "EventMessage",
"description": "A message for notifying the developer that an event that has occured (e.g. a compaction). Events are NOT part of the context window."
},
"ExportAgentRequest": {
"properties": {
"skills": {
"items": {
"$ref": "#/components/schemas/SkillSchema"
},
"type": "array",
"title": "Skills",
"description": "Skills to include in the export. Each skill must have a name and files (including SKILL.md)."
},
"conversation_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Conversation Id",
"description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history."
}
},
"type": "object",
"title": "ExportAgentRequest",
"description": "Request body for POST /export endpoint."
},
"FeedbackType": {
"type": "string",
"enum": ["positive", "negative"],
@@ -41771,6 +41862,46 @@
"required": ["query"],
"title": "SearchAllMessagesRequest"
},
"SkillSchema": {
"properties": {
"name": {
"type": "string",
"title": "Name",
"description": "Skill name, also serves as unique identifier (e.g., 'slack', 'pdf')"
},
"files": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"title": "Files",
"description": "Skill files as path -> content mapping. Must include 'SKILL.md' key if provided."
},
"source_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Source Url",
"description": "Source URL for skill resolution (e.g., 'letta:slack', 'anthropic:pdf', 'owner/repo/path')"
}
},
"type": "object",
"required": ["name"],
"title": "SkillSchema",
"description": "Skill schema for agent files.\n\nSkills are folders of instructions, scripts, and resources that agents can load.\nEither files (with SKILL.md) or source_url must be provided:\n- files with SKILL.md: inline skill content\n- source_url: reference to resolve later (e.g., 'letta:slack')\n- both: inline content with provenance tracking"
},
"SleeptimeManager": {
"properties": {
"manager_type": {

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
from letta.helpers.datetime_helpers import get_utc_time
from letta.schemas.agent import AgentState, CreateAgent
@@ -367,6 +367,37 @@ class ToolSchema(Tool):
return cls(**tool.model_dump())
class SkillSchema(BaseModel):
"""Skill schema for agent files.
Skills are folders of instructions, scripts, and resources that agents can load.
Either files (with SKILL.md) or source_url must be provided:
- files with SKILL.md: inline skill content
- source_url: reference to resolve later (e.g., 'letta:slack')
- both: inline content with provenance tracking
"""
name: str = Field(..., description="Skill name, also serves as unique identifier (e.g., 'slack', 'pdf')")
files: Optional[Dict[str, str]] = Field(
default=None,
description="Skill files as path -> content mapping. Must include 'SKILL.md' key if provided.",
)
source_url: Optional[str] = Field(
default=None,
description="Source URL for skill resolution (e.g., 'letta:slack', 'anthropic:pdf', 'owner/repo/path')",
)
@model_validator(mode="after")
def check_files_or_source_url(self) -> "SkillSchema":
"""Ensure either files (with SKILL.md) or source_url is provided."""
has_files = self.files and "SKILL.md" in self.files
has_source_url = self.source_url is not None
if not has_files and not has_source_url:
raise ValueError("Either files (with 'SKILL.md') or source_url must be provided")
return self
class MCPServerSchema(BaseModel):
"""MCP server schema for agent files with remapped ID."""
@@ -407,6 +438,7 @@ class AgentFileSchema(BaseModel):
sources: List[SourceSchema] = Field(..., description="List of sources in this agent file")
tools: List[ToolSchema] = Field(..., description="List of tools in this agent file")
mcp_servers: List[MCPServerSchema] = Field(..., description="List of MCP servers in this agent file")
skills: List[SkillSchema] = Field(default_factory=list, description="List of skills in this agent file")
metadata: Dict[str, str] = Field(
default_factory=dict, description="Metadata for this agent file, including revision_id and other export information."
)

View File

@@ -34,7 +34,7 @@ from letta.orm.errors import NoResultFound
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.agent_file import AgentFileSchema, SkillSchema
from letta.schemas.block import BaseBlock, Block, BlockResponse, BlockUpdate
from letta.schemas.enums import AgentType, MessageRole, RunStatus
from letta.schemas.file import AgentFileAttachment, FileMetadataBase, PaginatedAgentFiles
@@ -262,6 +262,47 @@ async def export_agent(
return agent_file_schema.model_dump()
class ExportAgentRequest(BaseModel):
"""Request body for POST /export endpoint."""
skills: List[SkillSchema] = Field(
default_factory=list,
description="Skills to include in the export. Each skill must have a name and files (including SKILL.md).",
)
conversation_id: Optional[str] = Field(
None,
description="Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history.",
)
@router.post("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent_with_skills")
async def export_agent_with_skills(
agent_id: str = AgentId,
request: Optional[ExportAgentRequest] = Body(default=None),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
) -> JSONResponse:
"""
Export the serialized JSON representation of an agent with optional skills.
This POST endpoint allows including skills in the export by providing them in the request body.
Skills are resolved client-side and passed as SkillSchema objects containing the skill files.
"""
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
# Use defaults if no request body provided
skills = request.skills if request else []
conversation_id = request.conversation_id if request else None
agent_file_schema = await server.agent_serialization_manager.export(
agent_ids=[agent_id],
actor=actor,
conversation_id=conversation_id,
skills=skills,
)
return agent_file_schema.model_dump()
class ImportedAgentsResponse(BaseModel):
"""Response model for imported agents"""

View File

@@ -25,6 +25,7 @@ from letta.schemas.agent_file import (
ImportResult,
MCPServerSchema,
MessageSchema,
SkillSchema,
SourceSchema,
ToolSchema,
)
@@ -359,7 +360,13 @@ class AgentSerializationManager:
logger.error(f"Failed to convert group {group.id}: {e}")
raise
async def export(self, agent_ids: List[str], actor: User, conversation_id: Optional[str] = None) -> AgentFileSchema:
async def export(
self,
agent_ids: List[str],
actor: User,
conversation_id: Optional[str] = None,
skills: Optional[List[SkillSchema]] = None,
) -> AgentFileSchema:
"""
Export agents and their related entities to AgentFileSchema format.
@@ -367,6 +374,8 @@ class AgentSerializationManager:
agent_ids: List of agent UUIDs to export
conversation_id: Optional conversation ID. If provided, uses the conversation's
in-context message_ids instead of the agent's global message_ids.
skills: Optional list of skills to include in the export. Skills are resolved
client-side and passed as SkillSchema objects.
Returns:
AgentFileSchema with all related entities
@@ -455,6 +464,7 @@ class AgentSerializationManager:
sources=source_schemas,
tools=tool_schemas,
mcp_servers=mcp_server_schemas,
skills=skills or [],
metadata={"revision_id": await get_latest_alembic_revision()},
created_at=datetime.now(timezone.utc),
)