feat: add skills support to agentfile (#9287)
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user