247 lines
9.1 KiB
Python
247 lines
9.1 KiB
Python
from datetime import datetime
|
|
from typing import Dict, List, Literal, Optional
|
|
|
|
from fastapi import APIRouter, Body, Depends, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
from letta import AgentState
|
|
from letta.errors import LettaInvalidArgumentError
|
|
from letta.schemas.agent import AgentRelationships
|
|
from letta.schemas.archive import Archive as PydanticArchive, ArchiveBase
|
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
from letta.schemas.passage import Passage
|
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
from letta.server.server import SyncServer
|
|
from letta.validators import AgentId, ArchiveId, PassageId
|
|
|
|
router = APIRouter(prefix="/archives", tags=["archives"])
|
|
|
|
|
|
class ArchiveCreateRequest(BaseModel):
|
|
"""Request model for creating an archive.
|
|
|
|
Intentionally excludes vector_db_provider. These are derived internally (vector DB provider from env).
|
|
"""
|
|
|
|
name: str
|
|
embedding_config: Optional[EmbeddingConfig] = Field(
|
|
None, description="Deprecated: Use `embedding` field instead. Embedding configuration for the archive", deprecated=True
|
|
)
|
|
embedding: Optional[str] = Field(None, description="Embedding model handle for the archive")
|
|
description: Optional[str] = None
|
|
|
|
|
|
class ArchiveUpdateRequest(BaseModel):
|
|
"""Request model for updating an archive (partial).
|
|
|
|
Supports updating only name and description.
|
|
"""
|
|
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
|
|
|
|
class PassageCreateRequest(BaseModel):
|
|
"""Request model for creating a passage in an archive."""
|
|
|
|
text: str = Field(..., description="The text content of the passage")
|
|
metadata: Optional[Dict] = Field(default=None, description="Optional metadata for the passage")
|
|
tags: Optional[List[str]] = Field(default=None, description="Optional tags for categorizing the passage")
|
|
|
|
|
|
@router.post("/", response_model=PydanticArchive, operation_id="create_archive")
|
|
async def create_archive(
|
|
archive: ArchiveCreateRequest = Body(...),
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Create a new archive.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
|
|
if archive.embedding_config is None and archive.embedding is None:
|
|
raise LettaInvalidArgumentError("Either embedding_config or embedding must be provided")
|
|
|
|
embedding_config = archive.embedding_config
|
|
if embedding_config is None and archive.embedding is not None:
|
|
handle = f"{archive.embedding.provider}/{archive.embedding.model}"
|
|
embedding_config = await server.get_embedding_config_from_handle_async(
|
|
handle=handle,
|
|
actor=actor,
|
|
)
|
|
|
|
return await server.archive_manager.create_archive_async(
|
|
name=archive.name,
|
|
embedding_config=embedding_config,
|
|
description=archive.description,
|
|
actor=actor,
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=List[PydanticArchive], operation_id="list_archives")
|
|
async def list_archives(
|
|
before: Optional[str] = Query(
|
|
None,
|
|
description="Archive ID cursor for pagination. Returns archives that come before this archive ID in the specified sort order",
|
|
),
|
|
after: Optional[str] = Query(
|
|
None,
|
|
description="Archive ID cursor for pagination. Returns archives that come after this archive ID in the specified sort order",
|
|
),
|
|
limit: Optional[int] = Query(50, description="Maximum number of archives to return"),
|
|
order: Literal["asc", "desc"] = Query(
|
|
"desc", description="Sort order for archives by creation time. 'asc' for oldest first, 'desc' for newest first"
|
|
),
|
|
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
|
name: Optional[str] = Query(None, description="Filter by archive name (exact match)"),
|
|
agent_id: Optional[str] = Query(None, description="Only archives attached to this agent ID"),
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Get a list of all archives for the current organization with optional filters and pagination.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
archives = await server.archive_manager.list_archives_async(
|
|
actor=actor,
|
|
before=before,
|
|
after=after,
|
|
limit=limit,
|
|
ascending=(order == "asc"),
|
|
name=name,
|
|
agent_id=agent_id,
|
|
)
|
|
return archives
|
|
|
|
|
|
@router.get("/{archive_id}", response_model=PydanticArchive, operation_id="retrieve_archive")
|
|
async def retrieve_archive(
|
|
archive_id: ArchiveId,
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Get a single archive by its ID.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
return await server.archive_manager.get_archive_by_id_async(
|
|
archive_id=archive_id,
|
|
actor=actor,
|
|
)
|
|
|
|
|
|
@router.patch("/{archive_id}", response_model=PydanticArchive, operation_id="modify_archive")
|
|
async def modify_archive(
|
|
archive_id: ArchiveId,
|
|
archive: ArchiveUpdateRequest = Body(...),
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Update an existing archive's name and/or description.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
return await server.archive_manager.update_archive_async(
|
|
archive_id=archive_id,
|
|
name=archive.name,
|
|
description=archive.description,
|
|
actor=actor,
|
|
)
|
|
|
|
|
|
@router.delete("/{archive_id}", response_model=PydanticArchive, operation_id="delete_archive")
|
|
async def delete_archive(
|
|
archive_id: ArchiveId,
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Delete an archive by its ID.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
return await server.archive_manager.delete_archive_async(
|
|
archive_id=archive_id,
|
|
actor=actor,
|
|
)
|
|
|
|
|
|
@router.get("/{archive_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_archive")
|
|
async def list_agents_for_archive(
|
|
archive_id: ArchiveId,
|
|
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"
|
|
),
|
|
include: List[AgentRelationships] = 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),
|
|
):
|
|
"""
|
|
Get a list of agents that have access to an archive with pagination support.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
return await server.archive_manager.get_agents_for_archive_async(
|
|
archive_id=archive_id,
|
|
actor=actor,
|
|
before=before,
|
|
after=after,
|
|
limit=limit,
|
|
include=include,
|
|
ascending=(order == "asc"),
|
|
)
|
|
|
|
|
|
@router.post("/{archive_id}/passages", response_model=Passage, operation_id="create_passage_in_archive")
|
|
async def create_passage_in_archive(
|
|
archive_id: ArchiveId,
|
|
passage: PassageCreateRequest = Body(...),
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Create a new passage in an archive.
|
|
|
|
This adds a passage to the archive and creates embeddings for vector storage.
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
return await server.archive_manager.create_passage_in_archive_async(
|
|
archive_id=archive_id,
|
|
text=passage.text,
|
|
metadata=passage.metadata,
|
|
tags=passage.tags,
|
|
actor=actor,
|
|
)
|
|
|
|
|
|
@router.delete("/{archive_id}/passages/{passage_id}", status_code=204, operation_id="delete_passage_from_archive")
|
|
async def delete_passage_from_archive(
|
|
archive_id: ArchiveId,
|
|
passage_id: PassageId,
|
|
server: "SyncServer" = Depends(get_letta_server),
|
|
headers: HeaderParams = Depends(get_headers),
|
|
):
|
|
"""
|
|
Delete a passage from an archive.
|
|
|
|
This permanently removes the passage from both the database and vector storage (if applicable).
|
|
"""
|
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
await server.archive_manager.delete_passage_from_archive_async(
|
|
archive_id=archive_id,
|
|
passage_id=passage_id,
|
|
actor=actor,
|
|
)
|
|
return None
|