Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/code/registry/0_registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ show_registry_contents(ScenarioRegistry.get_registry_singleton())
| Instantiation | Caller provides parameters | Pre-configured by initializer |
| When to use | Self-contained components with deferred configuration | Components requiring constructor parameters or compositional setup |

## Named Component Construction

Converter, target, and scorer registries use `InstanceHoldingRegistry` to build
components and store them in their `.instances` registry. Use
`create_named_instance(name=..., type_name=..., params=...)` to build and register
a component in one operation. The instance registry stores objects; it does not
construct them.

Duplicate names raise `ValueError`. Use `.instances.register(..., replace=True)`
only when replacement is intended. Converter and target registries also reject
reserved route names such as `catalog` and `types`. Use `.instances.unregister(name)`
to remove an instance.

Constructor annotations define parameter metadata and coercion. Use `Path` for a
local file input. Use `Path | str` when a component also supports a remote URL.
For this union, the registry preserves the supplied type: a `Path` stays a `Path`,
and a string stays a string. It never passes a URL through `Path`. Both union
orders have the same metadata, `type_name: "Path | str"`, including after a JSON
round-trip. Optional forms accept `None` in Python; the display type omits `None`,
as it does for other optional parameters.

The backend owns file-upload handling and cleanup, not the registry. See the
[registry API migration notes](../../gui/0_gui.md#registry-api-migration-notes)
for the REST contract and temporary compatibility behavior.

## See Also

- [Class Registries](1_class_registry.ipynb) - ScenarioRegistry, InitializerRegistry
Expand Down
27 changes: 27 additions & 0 deletions doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,33 @@ Use **Reload** to discard local edits and fetch the latest source content. Saved

---

## Registry API Migration Notes

Use `/api/converters/types` and `/api/targets/types` for registry build metadata.
These endpoints return all constructor parameters from the registry, including
lists, unions, and component references. The temporary `/catalog` routes retain
their scalar-only filtering for the current UI.
Create requests should supply an explicit registry `name`. Converter creation
returns the complete `ConverterInstance`; read its type from
`identifier.class_name`, not the old top-level `converter_type` field. Treat
returned IDs as opaque registry names, not UUIDs or identifier hashes.

Constructor parameters typed as `Path` accept base64 data-URI uploads through REST,
not server filesystem paths. Parameters typed as `Path | str` also accept Azure
Blob URLs. This applies to `AddImageVideoConverter.video_path` and
`ImageOverlayConverter.base_image`. Other local file inputs remain `Path`.
Uploads stay in backend-owned temporary storage until deletion or shutdown,
including with Azure-backed memory. Converter outputs still use configured result
storage. Uploads can contain any file type; the media endpoint renders only
allowlisted image, audio, and video extensions inline. Other files, including PDF,
SVG, HTML, text, and executables, download as `application/octet-stream` attachments.

**Temporary compatibility, scheduled for removal with the chat migration:**
the `/api/converters/catalog` and `/api/targets/catalog` routes project the same
registry metadata for the current UI. Create requests without a name receive a
generated `compat_...` name. New clients should not depend on these routes or
unnamed creation.

## Connection Health

CoPyRIT monitors the backend connection and shows a status banner:
Expand Down
10 changes: 9 additions & 1 deletion pyrit/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
version,
)
from pyrit.backend.services.configuration_file_service import ConfigurationFileService
from pyrit.backend.services.converter_service import get_converter_service
from pyrit.backend.services.environment_file_service import EnvironmentFileService
from pyrit.common.path import CONFIGURATION_DIRECTORY_PATH
from pyrit.registry import InitializerRegistry
Expand Down Expand Up @@ -110,7 +111,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# don't emit noise and don't perform filesystem side effects.
setup_frontend()

yield
converter_service = await asyncio.to_thread(get_converter_service)
try:
yield
finally:
try:
await converter_service.close_async()
finally:
get_converter_service.cache_clear()


app = FastAPI(
Expand Down
16 changes: 13 additions & 3 deletions pyrit/backend/mappers/converter_mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
from pyrit.models import ConverterIdentifier


def converter_object_to_instance(converter_id: str, converter_obj: Converter) -> ConverterInstance:
def converter_object_to_instance(
*,
converter_id: str,
converter_obj: Converter,
is_llm_based: bool,
description: str | None,
) -> ConverterInstance:
"""
Build a ConverterInstance DTO from a registry converter object.

Expand All @@ -24,13 +30,17 @@ def converter_object_to_instance(converter_id: str, converter_obj: Converter) ->
on the wire.

Args:
converter_id: The unique converter instance identifier.
converter_obj: The domain Converter object from the registry.
converter_id (str): The unique converter instance identifier.
converter_obj (Converter): The domain Converter object from the registry.
is_llm_based (bool): Whether the converter class requires an LLM target.
description (str | None): The converter class description.

Returns:
ConverterInstance DTO wrapping the converter's identifier.
"""
return ConverterInstance(
converter_id=converter_id,
identifier=ConverterIdentifier.from_component_identifier(converter_obj.get_identifier()),
is_llm_based=is_llm_based,
description=description,
)
4 changes: 4 additions & 0 deletions pyrit/backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
ConverterInstanceListResponse,
ConverterPreviewRequest,
ConverterPreviewResponse,
ConverterTypeEntry,
ConverterTypeResponse,
CreateConverterRequest,
CreateConverterResponse,
PreviewStep,
Expand Down Expand Up @@ -99,6 +101,8 @@
"ConverterInstanceListResponse": "pyrit.backend.models.converters",
"ConverterPreviewRequest": "pyrit.backend.models.converters",
"ConverterPreviewResponse": "pyrit.backend.models.converters",
"ConverterTypeEntry": "pyrit.backend.models.converters",
"ConverterTypeResponse": "pyrit.backend.models.converters",
"CreateConverterRequest": "pyrit.backend.models.converters",
"CreateConverterResponse": "pyrit.backend.models.converters",
"PreviewStep": "pyrit.backend.models.converters",
Expand Down
2 changes: 2 additions & 0 deletions pyrit/backend/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

from pydantic import BaseModel, Field

REGISTRY_INSTANCE_NAME_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"


class PaginationInfo(BaseModel):
"""Pagination metadata for list responses."""
Expand Down
39 changes: 33 additions & 6 deletions pyrit/backend/models/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@

from pydantic import BaseModel, Field

from pyrit.backend.models.common import REGISTRY_INSTANCE_NAME_PATTERN
from pyrit.models import ConverterIdentifier, Parameter, PromptDataType

__all__ = [
"ConverterCatalogEntry",
"ConverterCatalogResponse",
"ConverterInstance",
"ConverterInstanceListResponse",
"ConverterTypeEntry",
"ConverterTypeResponse",
"CreateConverterRequest",
"CreateConverterResponse",
"ConverterPreviewRequest",
Expand All @@ -27,11 +30,11 @@


# ============================================================================
# Converter Catalog (Available Types)
# Converter Types
# ============================================================================


class ConverterCatalogEntry(BaseModel):
class ConverterTypeEntry(BaseModel):
"""A converter type available from the backend registry."""

converter_type: str = Field(..., description="Converter class name (e.g., 'Base64Converter')")
Expand All @@ -48,10 +51,17 @@ class ConverterCatalogEntry(BaseModel):
description: str | None = Field(None, description="Short description of the converter from its docstring")


class ConverterCatalogResponse(BaseModel):
class ConverterTypeResponse(BaseModel):
"""Response for listing available converter types from the registry."""

items: list[ConverterCatalogEntry] = Field(..., description="List of available converter types")
items: list[ConverterTypeEntry] = Field(..., description="List of available converter types")


# LEGACY COMPATIBILITY: ``Catalog`` is the pre-registry name for ``Type``. These
# aliases exist only so the un-migrated chat UI keeps working; delete them with the
# /catalog route when that UI switches to the /types API.
ConverterCatalogEntry = ConverterTypeEntry
ConverterCatalogResponse = ConverterTypeResponse


# ============================================================================
Expand All @@ -68,8 +78,10 @@ class ConverterInstance(BaseModel):
for the converter's class, supported data types, and constructor params.
"""

converter_id: str = Field(..., description="Unique converter instance identifier")
converter_id: str = Field(..., description="Converter instance registry name")
identifier: ConverterIdentifier = Field(..., description="The converter's identity/configuration projection")
is_llm_based: bool = Field(False, description="Whether this converter requires an LLM target")
description: str | None = Field(None, description="Short description of the converter type")


class ConverterInstanceListResponse(BaseModel):
Expand All @@ -81,7 +93,17 @@ class ConverterInstanceListResponse(BaseModel):
class CreateConverterRequest(BaseModel):
"""Request to create a new converter instance."""

# LEGACY COMPATIBILITY: The current chat UI does not send a name. Make this
# field required when the chat-migration stack layer sends explicit names.
name: str | None = Field(
None,
min_length=1,
pattern=REGISTRY_INSTANCE_NAME_PATTERN,
description="Unique registry name; omitted only for legacy chat compatibility",
)
type: str = Field(..., description="Converter type (e.g., 'Base64Converter')")
# LEGACY COMPATIBILITY: The former create response echoed this field. Remove
# it after clients use the complete ConverterInstance response.
display_name: str | None = Field(None, description="Human-readable display name")
params: dict[str, Any] = Field(
default_factory=dict,
Expand All @@ -90,7 +112,12 @@ class CreateConverterRequest(BaseModel):


class CreateConverterResponse(BaseModel):
"""Response after creating a converter instance."""
"""
Legacy response model for downstream imports.

POST /converters now returns ``ConverterInstance``. Remove this model when
downstream clients no longer import the former response type.
"""

converter_id: str = Field(..., description="Unique converter instance identifier")
converter_type: str = Field(..., description="Converter class name")
Expand Down
25 changes: 21 additions & 4 deletions pyrit/backend/models/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from pydantic import BaseModel, Field

from pyrit.backend.models.common import PaginationInfo
from pyrit.backend.models.common import REGISTRY_INSTANCE_NAME_PATTERN, PaginationInfo
from pyrit.models import JSONValue, Parameter
from pyrit.models.catalog.target import TargetInstance

Expand All @@ -21,14 +21,16 @@
"TargetCatalogEntry",
"TargetCatalogResponse",
"TargetListResponse",
"TargetTypeEntry",
"TargetTypeResponse",
]


def _default_auth_modes() -> list[Literal["api_key", "identity"]]:
return ["api_key"]


class TargetCatalogEntry(BaseModel):
class TargetTypeEntry(BaseModel):
"""A target type available from the backend registry."""

target_type: str = Field(..., description="Target class name (e.g., 'OpenAIChatTarget')")
Expand All @@ -43,10 +45,17 @@ class TargetCatalogEntry(BaseModel):
description: str | None = Field(None, description="Short description of the target from its docstring")


class TargetCatalogResponse(BaseModel):
class TargetTypeResponse(BaseModel):
"""Response for listing available target types from the registry."""

items: list[TargetCatalogEntry] = Field(..., description="List of available target types")
items: list[TargetTypeEntry] = Field(..., description="List of available target types")


# LEGACY COMPATIBILITY: ``Catalog`` is the pre-registry name for ``Type``. These
# aliases exist only so the un-migrated configuration UI keeps working; delete them
# with the /catalog route when that UI switches to the /types API.
TargetCatalogEntry = TargetTypeEntry
TargetCatalogResponse = TargetTypeResponse


class TargetListResponse(BaseModel):
Expand All @@ -59,6 +68,14 @@ class TargetListResponse(BaseModel):
class CreateTargetRequest(BaseModel):
"""Request to create a new target instance."""

# LEGACY COMPATIBILITY: The current target configuration UI does not send a
# name. Make this field required after that UI sends explicit registry names.
name: str | None = Field(
None,
min_length=1,
pattern=REGISTRY_INSTANCE_NAME_PATTERN,
description="Unique registry name; omitted only for legacy UI compatibility",
)
type: str = Field(..., description="Target type (e.g., 'OpenAIChatTarget')")
params: dict[str, JSONValue] = Field(default_factory=dict, description="Target constructor parameters")
auth_mode: Literal["api_key", "identity"] = Field(
Expand Down
Loading
Loading