跳转至

API reference

Generated from the apogee-ai-prompt source with mkdocstrings. Every symbol below is exported from apogee_ai_prompt, so it is part of the supported public surface.

Application · DTOs

CreatePromptDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

kind class-attribute instance-attribute

Python
kind: PromptKind = CHAT

name class-attribute instance-attribute

Python
name: str | None = None

description class-attribute instance-attribute

Python
description: str | None = None

tags class-attribute instance-attribute

Python
tags: list[str] = Field(default_factory=list)

body instance-attribute

Python
body: str

variables class-attribute instance-attribute

Python
variables: list[PromptVariableDTO] = Field(default_factory=list)

engine class-attribute instance-attribute

Python
engine: TemplateEngineKind = JINJA

model class-attribute instance-attribute

Python
model: str | None = None

provider class-attribute instance-attribute

Python
provider: str | None = None

created_by class-attribute instance-attribute

Python
created_by: str | None = None

notes class-attribute instance-attribute

Python
notes: str | None = None

DiffPromptsDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

from_version class-attribute instance-attribute

Python
from_version: int = Field(ge=1)

to_version class-attribute instance-attribute

Python
to_version: int = Field(ge=1)

LintPromptDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

version class-attribute instance-attribute

Python
version: int | None = None

ListPromptsFilterDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

tag class-attribute instance-attribute

Python
tag: str | None = None

limit class-attribute instance-attribute

Python
limit: int | None = None

offset class-attribute instance-attribute

Python
offset: int = 0

PromptOutputDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

kind instance-attribute

Python
kind: PromptKind

name class-attribute instance-attribute

Python
name: str | None = None

description class-attribute instance-attribute

Python
description: str | None = None

tags class-attribute instance-attribute

Python
tags: list[str] = Field(default_factory=list)

versions class-attribute instance-attribute

Python
versions: list[PromptVersionDTO] = Field(default_factory=list)

current_version class-attribute instance-attribute

Python
current_version: int | None = None

PromptVariableDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

name instance-attribute

Python
name: str

type class-attribute instance-attribute

Python
type: VariableType = STRING

required class-attribute instance-attribute

Python
required: bool = True

default class-attribute instance-attribute

Python
default: Any = None

description class-attribute instance-attribute

Python
description: str | None = None

PromptVersionDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

version class-attribute instance-attribute

Python
version: int = Field(ge=1)

body instance-attribute

Python
body: str

variables class-attribute instance-attribute

Python
variables: list[PromptVariableDTO] = Field(default_factory=list)

engine class-attribute instance-attribute

Python
engine: TemplateEngineKind = JINJA

model class-attribute instance-attribute

Python
model: str | None = None

provider class-attribute instance-attribute

Python
provider: str | None = None

status class-attribute instance-attribute

Python
status: PromptStatus = DRAFT

created_by class-attribute instance-attribute

Python
created_by: str | None = None

notes class-attribute instance-attribute

Python
notes: str | None = None

metadata class-attribute instance-attribute

Python
metadata: dict[str, str] = Field(default_factory=dict)

PublishPromptDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

version class-attribute instance-attribute

Python
version: int = Field(ge=1)

set_current class-attribute instance-attribute

Python
set_current: bool = True

skip_lint class-attribute instance-attribute

Python
skip_lint: bool = False

RenderPromptDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

version class-attribute instance-attribute

Python
version: int | None = None

variables class-attribute instance-attribute

Python
variables: dict[str, Any] = Field(default_factory=dict)

routing_value class-attribute instance-attribute

Python
routing_value: str | None = None

Optional routing key (user_id/tenant) used by canary resolver.

strict class-attribute instance-attribute

Python
strict: bool = True

If True, raise on unknown or missing required variables.

RenderedPromptDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

version instance-attribute

Python
version: int

text instance-attribute

Python
text: str

variables_used class-attribute instance-attribute

Python
variables_used: dict[str, Any] = Field(default_factory=dict)

engine class-attribute instance-attribute

Python
engine: str = 'jinja'

RollbackPromptDTO

Bases: BaseModel

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

to_version class-attribute instance-attribute

Python
to_version: int = Field(ge=1)

UpdatePromptDTO

Bases: BaseModel

Adds a new version to an existing prompt.

model_config class-attribute instance-attribute

Python
model_config = ConfigDict(extra='forbid')

slug instance-attribute

Python
slug: str

body instance-attribute

Python
body: str

variables class-attribute instance-attribute

Python
variables: list[PromptVariableDTO] = Field(default_factory=list)

engine class-attribute instance-attribute

Python
engine: TemplateEngineKind | None = None

model class-attribute instance-attribute

Python
model: str | None = None

provider class-attribute instance-attribute

Python
provider: str | None = None

created_by class-attribute instance-attribute

Python
created_by: str | None = None

notes class-attribute instance-attribute

Python
notes: str | None = None

Application · Use cases

CreatePromptUseCase

Python
CreatePromptUseCase(repository: IPromptRepository)
Source code in apogee_ai_prompt/application/use_cases/create_prompt_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(dto: CreatePromptDTO) -> PromptOutputDTO
Source code in apogee_ai_prompt/application/use_cases/create_prompt_use_case.py
Python
async def execute(self, dto: CreatePromptDTO) -> PromptOutputDTO:
    if await self._repository.exists(dto.slug):
        raise PromptAlreadyExistsException(dto.slug)
    first_version = PromptVersion(
        version=1,
        body=dto.body,
        variables=tuple(variable_from_dto(v) for v in dto.variables),
        engine=dto.engine,
        model=dto.model,
        provider=dto.provider,
        created_by=dto.created_by,
        notes=dto.notes,
    )
    prompt = Prompt(
        slug=dto.slug,
        kind=dto.kind,
        name=dto.name,
        description=dto.description,
        tags=tuple(dto.tags),
        versions=(first_version,),
        current_version=None,
    )
    saved = await self._repository.save(prompt)
    return prompt_to_output_dto(saved)

DeletePromptUseCase

Python
DeletePromptUseCase(repository: IPromptRepository)
Source code in apogee_ai_prompt/application/use_cases/delete_prompt_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(slug: str) -> None
Source code in apogee_ai_prompt/application/use_cases/delete_prompt_use_case.py
Python
async def execute(self, slug: str) -> None:
    if not await self._repository.exists(slug):
        raise PromptNotFoundException(slug)
    await self._repository.delete(slug)

DiffPromptsUseCase

Python
DiffPromptsUseCase(repository: IPromptRepository)
Source code in apogee_ai_prompt/application/use_cases/diff_prompts_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(dto: DiffPromptsDTO) -> PromptDiff
Source code in apogee_ai_prompt/application/use_cases/diff_prompts_use_case.py
Python
async def execute(self, dto: DiffPromptsDTO) -> PromptDiff:
    prompt = await self._repository.find(dto.slug)
    if prompt is None:
        raise PromptNotFoundException(dto.slug)
    if not prompt.has_version(dto.from_version):
        raise PromptVersionNotFoundException(dto.slug, dto.from_version)
    if not prompt.has_version(dto.to_version):
        raise PromptVersionNotFoundException(dto.slug, dto.to_version)
    a = prompt.get_version(dto.from_version)
    b = prompt.get_version(dto.to_version)
    unified = "".join(
        difflib.unified_diff(
            a.body.splitlines(keepends=True),
            b.body.splitlines(keepends=True),
            fromfile=f"{dto.slug}@v{dto.from_version}",
            tofile=f"{dto.slug}@v{dto.to_version}",
            lineterm="",
        )
    )
    a_vars = {v.name for v in a.variables}
    b_vars = {v.name for v in b.variables}
    return PromptDiff(
        slug=dto.slug,
        from_version=dto.from_version,
        to_version=dto.to_version,
        unified_diff=unified,
        added_variables=tuple(sorted(b_vars - a_vars)),
        removed_variables=tuple(sorted(a_vars - b_vars)),
        body_changed=a.body != b.body,
    )

GetPromptUseCase

Python
GetPromptUseCase(repository: IPromptRepository)
Source code in apogee_ai_prompt/application/use_cases/get_prompt_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(slug: str) -> PromptOutputDTO
Source code in apogee_ai_prompt/application/use_cases/get_prompt_use_case.py
Python
async def execute(self, slug: str) -> PromptOutputDTO:
    prompt = await self._repository.find(slug)
    if prompt is None:
        raise PromptNotFoundException(slug)
    return prompt_to_output_dto(prompt)

LintPromptUseCase

Python
LintPromptUseCase(repository: IPromptRepository, linter: IPromptLinter)
Source code in apogee_ai_prompt/application/use_cases/lint_prompt_use_case.py
Python
def __init__(self, repository: IPromptRepository, linter: IPromptLinter) -> None:
    self._repository = repository
    self._linter = linter

execute async

Python
execute(dto: LintPromptDTO) -> LintReport
Source code in apogee_ai_prompt/application/use_cases/lint_prompt_use_case.py
Python
async def execute(self, dto: LintPromptDTO) -> LintReport:
    prompt = await self._repository.find(dto.slug)
    if prompt is None:
        raise PromptNotFoundException(dto.slug)
    version_number = dto.version
    if version_number is None:
        latest = prompt.latest_version()
        if latest is None:
            raise PromptVersionNotFoundException(dto.slug, 0)
        version_number = latest.version
    if not prompt.has_version(version_number):
        raise PromptVersionNotFoundException(dto.slug, version_number)
    version = prompt.get_version(version_number)
    return self._linter.lint(dto.slug, version)

ListPromptsUseCase

Python
ListPromptsUseCase(repository: IPromptRepository)
Source code in apogee_ai_prompt/application/use_cases/list_prompts_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(filt: ListPromptsFilterDTO | None = None) -> list[PromptOutputDTO]
Source code in apogee_ai_prompt/application/use_cases/list_prompts_use_case.py
Python
async def execute(self, filt: ListPromptsFilterDTO | None = None) -> list[PromptOutputDTO]:
    filt = filt or ListPromptsFilterDTO()
    prompts = await self._repository.list(
        tag=filt.tag,
        limit=filt.limit,
        offset=filt.offset,
    )
    return [prompt_to_output_dto(p) for p in prompts]

PublishPromptUseCase

Python
PublishPromptUseCase(repository: IPromptRepository, linter: IPromptLinter | None = None)
Source code in apogee_ai_prompt/application/use_cases/publish_prompt_use_case.py
Python
def __init__(
    self,
    repository: IPromptRepository,
    linter: IPromptLinter | None = None,
) -> None:
    self._repository = repository
    self._linter = linter

execute async

Python
execute(dto: PublishPromptDTO) -> PromptOutputDTO
Source code in apogee_ai_prompt/application/use_cases/publish_prompt_use_case.py
Python
async def execute(self, dto: PublishPromptDTO) -> PromptOutputDTO:
    prompt = await self._repository.find(dto.slug)
    if prompt is None:
        raise PromptNotFoundException(dto.slug)
    if not prompt.has_version(dto.version):
        raise PromptVersionNotFoundException(dto.slug, dto.version)
    version = prompt.get_version(dto.version)

    if self._linter is not None and not dto.skip_lint:
        report = self._linter.lint(dto.slug, version)
        if report.has_errors:
            error_count = sum(1 for i in report.issues if i.severity.value == "error")
            raise LintBlockedException(dto.slug, dto.version, error_count)

    published = version.with_status(PromptStatus.PUBLISHED)
    updated = prompt.with_replaced_version(published)
    if dto.set_current:
        updated = updated.with_current_version(dto.version)
    saved = await self._repository.save(updated)
    return prompt_to_output_dto(saved)

RenderPromptUseCase

Python
RenderPromptUseCase(repository: IPromptRepository, engines: Mapping[TemplateEngineKind, ITemplateEngine], *, canary_configs: Mapping[str, CanaryConfig] | None = None, variant_resolver: IVariantResolver | None = None)

Render a Prompt by slug+version, optionally routed via canary.

Source code in apogee_ai_prompt/application/use_cases/render_prompt_use_case.py
Python
def __init__(
    self,
    repository: IPromptRepository,
    engines: Mapping[TemplateEngineKind, ITemplateEngine],
    *,
    canary_configs: Mapping[str, CanaryConfig] | None = None,
    variant_resolver: IVariantResolver | None = None,
) -> None:
    self._repository = repository
    self._engines = dict(engines)
    self._canary = dict(canary_configs or {})
    self._resolver = variant_resolver

execute async

Python
execute(dto: RenderPromptDTO) -> RenderedPrompt
Source code in apogee_ai_prompt/application/use_cases/render_prompt_use_case.py
Python
async def execute(self, dto: RenderPromptDTO) -> RenderedPrompt:
    prompt = await self._repository.find(dto.slug)
    if prompt is None:
        raise PromptNotFoundException(dto.slug)

    version = self._select_version(prompt, dto)
    coerced = validate_and_coerce_variables(version, dto.variables, strict=dto.strict)

    engine = self._engines.get(version.engine)
    if engine is None:
        raise KeyError(f"No template engine registered for {version.engine.value!r}")
    text = engine.render(version.body, coerced)
    return RenderedPrompt(
        slug=dto.slug,
        version=version.version,
        text=text,
        variables_used=coerced,
        engine=engine.name,
    )

ResolveVariantUseCase

Python
ResolveVariantUseCase(resolver: IVariantResolver)
Source code in apogee_ai_prompt/application/use_cases/resolve_variant_use_case.py
Python
def __init__(self, resolver: IVariantResolver) -> None:
    self._resolver = resolver

execute

Python
execute(config: CanaryConfig, *, routing_value: str) -> VariantSelection
Source code in apogee_ai_prompt/application/use_cases/resolve_variant_use_case.py
Python
def execute(self, config: CanaryConfig, *, routing_value: str) -> VariantSelection:
    return self._resolver.resolve(config, routing_value=routing_value)

RollbackPromptUseCase

Python
RollbackPromptUseCase(repository: IPromptRepository)

Sets current_version to a previously published version.

Source code in apogee_ai_prompt/application/use_cases/rollback_prompt_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(dto: RollbackPromptDTO) -> PromptOutputDTO
Source code in apogee_ai_prompt/application/use_cases/rollback_prompt_use_case.py
Python
async def execute(self, dto: RollbackPromptDTO) -> PromptOutputDTO:
    prompt = await self._repository.find(dto.slug)
    if prompt is None:
        raise PromptNotFoundException(dto.slug)
    if not prompt.has_version(dto.to_version):
        raise PromptVersionNotFoundException(dto.slug, dto.to_version)
    updated = prompt.with_current_version(dto.to_version)
    saved = await self._repository.save(updated)
    return prompt_to_output_dto(saved)

UpdatePromptUseCase

Python
UpdatePromptUseCase(repository: IPromptRepository)

Adds a new DRAFT version to an existing prompt.

Source code in apogee_ai_prompt/application/use_cases/update_prompt_use_case.py
Python
def __init__(self, repository: IPromptRepository) -> None:
    self._repository = repository

execute async

Python
execute(dto: UpdatePromptDTO) -> PromptOutputDTO
Source code in apogee_ai_prompt/application/use_cases/update_prompt_use_case.py
Python
async def execute(self, dto: UpdatePromptDTO) -> PromptOutputDTO:
    prompt = await self._repository.find(dto.slug)
    if prompt is None:
        raise PromptNotFoundException(dto.slug)
    latest = prompt.latest_version()
    engine = dto.engine or (latest.engine if latest else TemplateEngineKind.JINJA)
    new_version = PromptVersion(
        version=prompt.next_version_number(),
        body=dto.body,
        variables=tuple(variable_from_dto(v) for v in dto.variables),
        engine=engine,
        model=dto.model or (latest.model if latest else None),
        provider=dto.provider or (latest.provider if latest else None),
        created_by=dto.created_by,
        notes=dto.notes,
    )
    updated = prompt.with_added_version(new_version)
    saved = await self._repository.save(updated)
    return prompt_to_output_dto(saved)

Domain

CanaryConfig dataclass

Python
CanaryConfig(slug: str, variants: dict[int, float] = dict(), routing_key: str = 'user_id')

Weighted distribution of versions for a canary release.

variants maps version number → weight (must sum to 1.0). The resolver uses a deterministic hash of the routing key (user_id / tenant_id / etc.) to assign requests to versions, so the same key always lands in the same bucket.

slug instance-attribute

Python
slug: str

variants class-attribute instance-attribute

Python
variants: dict[int, float] = field(default_factory=dict)

routing_key class-attribute instance-attribute

Python
routing_key: str = 'user_id'

CostEstimate dataclass

Python
CostEstimate(provider: str, model: str, input_tokens: int, output_tokens: int, input_cost_usd: float, output_cost_usd: float)

Cost estimate in USD given input/output token assumptions.

provider instance-attribute

Python
provider: str

model instance-attribute

Python
model: str

input_tokens instance-attribute

Python
input_tokens: int

output_tokens instance-attribute

Python
output_tokens: int

input_cost_usd instance-attribute

Python
input_cost_usd: float

output_cost_usd instance-attribute

Python
output_cost_usd: float

total_cost_usd property

Python
total_cost_usd: float

LintIssue dataclass

Python
LintIssue(rule: str, severity: LintSeverity, message: str, line: int | None = None)

rule instance-attribute

Python
rule: str

severity instance-attribute

Python
severity: LintSeverity

message instance-attribute

Python
message: str

line class-attribute instance-attribute

Python
line: int | None = None

LintReport dataclass

Python
LintReport(slug: str, version: int, issues: tuple[LintIssue, ...] = tuple())

slug instance-attribute

Python
slug: str

version instance-attribute

Python
version: int

issues class-attribute instance-attribute

Python
issues: tuple[LintIssue, ...] = field(default_factory=tuple)

has_errors property

Python
has_errors: bool

has_warnings property

Python
has_warnings: bool

of_severity

Python
of_severity(severity: LintSeverity) -> tuple[LintIssue, ...]
Source code in apogee_ai_prompt/domain/value_objects/lint_issue.py
Python
def of_severity(self, severity: LintSeverity) -> tuple[LintIssue, ...]:
    return tuple(i for i in self.issues if i.severity == severity)

LintSeverity

Bases: str, Enum

INFO class-attribute instance-attribute

Python
INFO = 'info'

WARNING class-attribute instance-attribute

Python
WARNING = 'warning'

ERROR class-attribute instance-attribute

Python
ERROR = 'error'

Prompt dataclass

Python
Prompt(slug: str, kind: PromptKind = CHAT, name: str | None = None, description: str | None = None, tags: tuple[str, ...] = tuple(), versions: tuple[PromptVersion, ...] = tuple(), current_version: int | None = None, created_at: datetime = (lambda: now(utc))(), updated_at: datetime = (lambda: now(utc))())

Aggregate root: a named prompt identified by slug with N versions.

The prompt itself is a thin shell carrying the slug, kind, descriptive metadata and the ordered tuple of versions. Business rules around publishing, rolling back and version pinning are enforced at this level.

slug instance-attribute

Python
slug: str

kind class-attribute instance-attribute

Python
kind: PromptKind = CHAT

name class-attribute instance-attribute

Python
name: str | None = None

description class-attribute instance-attribute

Python
description: str | None = None

tags class-attribute instance-attribute

Python
tags: tuple[str, ...] = field(default_factory=tuple)

versions class-attribute instance-attribute

Python
versions: tuple[PromptVersion, ...] = field(default_factory=tuple)

current_version class-attribute instance-attribute

Python
current_version: int | None = None

created_at class-attribute instance-attribute

Python
created_at: datetime = field(default_factory=lambda: now(utc))

updated_at class-attribute instance-attribute

Python
updated_at: datetime = field(default_factory=lambda: now(utc))

get_version

Python
get_version(version: int) -> PromptVersion
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def get_version(self, version: int) -> PromptVersion:
    for v in self.versions:
        if v.version == version:
            return v
    raise KeyError(f"Version {version} not found in prompt {self.slug!r}")

has_version

Python
has_version(version: int) -> bool
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def has_version(self, version: int) -> bool:
    return any(v.version == version for v in self.versions)

latest_version

Python
latest_version() -> PromptVersion | None
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def latest_version(self) -> PromptVersion | None:
    return self.versions[-1] if self.versions else None

published_versions

Python
published_versions() -> tuple[PromptVersion, ...]
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def published_versions(self) -> tuple[PromptVersion, ...]:
    return tuple(v for v in self.versions if v.status == PromptStatus.PUBLISHED)

latest_published

Python
latest_published() -> PromptVersion | None
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def latest_published(self) -> PromptVersion | None:
    published = self.published_versions()
    return published[-1] if published else None

with_added_version

Python
with_added_version(new_version: PromptVersion) -> Prompt
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def with_added_version(self, new_version: PromptVersion) -> Prompt:
    if any(v.version == new_version.version for v in self.versions):
        raise ValueError(
            f"Version {new_version.version} already exists in prompt {self.slug!r}"
        )
    new_versions = tuple(sorted([*self.versions, new_version], key=lambda v: v.version))
    return replace(
        self,
        versions=new_versions,
        updated_at=datetime.now(timezone.utc),
    )

with_replaced_version

Python
with_replaced_version(replacement: PromptVersion) -> Prompt
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def with_replaced_version(self, replacement: PromptVersion) -> Prompt:
    if not any(v.version == replacement.version for v in self.versions):
        raise KeyError(f"Version {replacement.version} not found")
    new_versions = tuple(
        replacement if v.version == replacement.version else v for v in self.versions
    )
    return replace(
        self,
        versions=new_versions,
        updated_at=datetime.now(timezone.utc),
    )

with_current_version

Python
with_current_version(version: int) -> Prompt
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def with_current_version(self, version: int) -> Prompt:
    if not self.has_version(version):
        raise KeyError(f"Version {version} not found")
    return replace(
        self,
        current_version=version,
        updated_at=datetime.now(timezone.utc),
    )

next_version_number

Python
next_version_number() -> int
Source code in apogee_ai_prompt/domain/entities/prompt.py
Python
def next_version_number(self) -> int:
    if not self.versions:
        return 1
    return max(v.version for v in self.versions) + 1

PromptDiff dataclass

Python
PromptDiff(slug: str, from_version: int, to_version: int, unified_diff: str, added_variables: tuple[str, ...] = (), removed_variables: tuple[str, ...] = (), body_changed: bool = True)

slug instance-attribute

Python
slug: str

from_version instance-attribute

Python
from_version: int

to_version instance-attribute

Python
to_version: int

unified_diff instance-attribute

Python
unified_diff: str

added_variables class-attribute instance-attribute

Python
added_variables: tuple[str, ...] = ()

removed_variables class-attribute instance-attribute

Python
removed_variables: tuple[str, ...] = ()

body_changed class-attribute instance-attribute

Python
body_changed: bool = True

PromptStatus

Bases: str, Enum

DRAFT class-attribute instance-attribute

Python
DRAFT = 'draft'

PUBLISHED class-attribute instance-attribute

Python
PUBLISHED = 'published'

DEPRECATED class-attribute instance-attribute

Python
DEPRECATED = 'deprecated'

ARCHIVED class-attribute instance-attribute

Python
ARCHIVED = 'archived'

PromptVariable dataclass

Python
PromptVariable(name: str, type: VariableType, required: bool = True, default: Any = None, description: str | None = None)

Schema declaration for a single variable consumed by a PromptVersion.

name instance-attribute

Python
name: str

type instance-attribute

Python
type: VariableType

required class-attribute instance-attribute

Python
required: bool = True

default class-attribute instance-attribute

Python
default: Any = None

description class-attribute instance-attribute

Python
description: str | None = None

PromptVersion dataclass

Python
PromptVersion(version: int, body: str, variables: tuple[PromptVariable, ...] = tuple(), engine: TemplateEngineKind = JINJA, model: str | None = None, provider: str | None = None, status: PromptStatus = DRAFT, created_at: datetime = (lambda: now(utc))(), created_by: str | None = None, notes: str | None = None, metadata: dict[str, str] = dict())

Single immutable version of a Prompt.

A PromptVersion captures the complete state of a prompt at a point in time: template body, schema of variables, target model/provider, render engine, plus status and authorship metadata. Once status reaches PUBLISHED it must not be mutated — new edits create a new version.

version instance-attribute

Python
version: int

body instance-attribute

Python
body: str

variables class-attribute instance-attribute

Python
variables: tuple[PromptVariable, ...] = field(default_factory=tuple)

engine class-attribute instance-attribute

Python
engine: TemplateEngineKind = JINJA

model class-attribute instance-attribute

Python
model: str | None = None

provider class-attribute instance-attribute

Python
provider: str | None = None

status class-attribute instance-attribute

Python
status: PromptStatus = DRAFT

created_at class-attribute instance-attribute

Python
created_at: datetime = field(default_factory=lambda: now(utc))

created_by class-attribute instance-attribute

Python
created_by: str | None = None

notes class-attribute instance-attribute

Python
notes: str | None = None

metadata class-attribute instance-attribute

Python
metadata: dict[str, str] = field(default_factory=dict)

is_published property

Python
is_published: bool

with_status

Python
with_status(status: PromptStatus) -> PromptVersion
Source code in apogee_ai_prompt/domain/entities/prompt_version.py
Python
def with_status(self, status: PromptStatus) -> PromptVersion:
    return PromptVersion(
        version=self.version,
        body=self.body,
        variables=self.variables,
        engine=self.engine,
        model=self.model,
        provider=self.provider,
        status=status,
        created_at=self.created_at,
        created_by=self.created_by,
        notes=self.notes,
        metadata=self.metadata,
    )

RenderedPrompt dataclass

Python
RenderedPrompt(slug: str, version: int, text: str, variables_used: dict[str, Any] = dict(), engine: str = 'jinja')

slug instance-attribute

Python
slug: str

version instance-attribute

Python
version: int

text instance-attribute

Python
text: str

variables_used class-attribute instance-attribute

Python
variables_used: dict[str, Any] = field(default_factory=dict)

engine class-attribute instance-attribute

Python
engine: str = 'jinja'

TokenCount dataclass

Python
TokenCount(tokens: int, counter: str = 'heuristic')

tokens instance-attribute

Python
tokens: int

counter class-attribute instance-attribute

Python
counter: str = 'heuristic'

VariantSelection dataclass

Python
VariantSelection(slug: str, selected_version: int, routing_key: str, routing_value: str, bucket: float)

slug instance-attribute

Python
slug: str

selected_version instance-attribute

Python
selected_version: int

routing_key instance-attribute

Python
routing_key: str

routing_value instance-attribute

Python
routing_value: str

bucket instance-attribute

Python
bucket: float

Domain · Enums

PromptKind

Bases: str, Enum

CHAT class-attribute instance-attribute

Python
CHAT = 'chat'

COMPLETION class-attribute instance-attribute

Python
COMPLETION = 'completion'

SYSTEM class-attribute instance-attribute

Python
SYSTEM = 'system'

TOOL_CALL class-attribute instance-attribute

Python
TOOL_CALL = 'tool_call'

TemplateEngineKind

Bases: str, Enum

JINJA class-attribute instance-attribute

Python
JINJA = 'jinja'

MUSTACHE class-attribute instance-attribute

Python
MUSTACHE = 'mustache'

PLAIN class-attribute instance-attribute

Python
PLAIN = 'plain'

VariableType

Bases: str, Enum

STRING class-attribute instance-attribute

Python
STRING = 'string'

INT class-attribute instance-attribute

Python
INT = 'int'

FLOAT class-attribute instance-attribute

Python
FLOAT = 'float'

BOOL class-attribute instance-attribute

Python
BOOL = 'bool'

LIST class-attribute instance-attribute

Python
LIST = 'list'

DICT class-attribute instance-attribute

Python
DICT = 'dict'

Domain · Exceptions

LintBlockedException

Python
LintBlockedException(slug: str, version: int, errors: int)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, slug: str, version: int, errors: int) -> None:
    super().__init__(
        f"Prompt {slug!r} version {version} blocked by linter ({errors} error(s))"
    )
    self.slug = slug
    self.version = version
    self.errors = errors

slug instance-attribute

Python
slug = slug

version instance-attribute

Python
version = version

errors instance-attribute

Python
errors = errors

PromptAlreadyExistsException

Python
PromptAlreadyExistsException(slug: str)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, slug: str) -> None:
    super().__init__(f"Prompt {slug!r} already exists")
    self.slug = slug

slug instance-attribute

Python
slug = slug

PromptError

Bases: Exception

Base for all apogee-ai-prompt errors.

PromptNotFoundException

Python
PromptNotFoundException(slug: str)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, slug: str) -> None:
    super().__init__(f"Prompt {slug!r} not found")
    self.slug = slug

slug instance-attribute

Python
slug = slug

PromptValidationException

Python
PromptValidationException(message: str, slug: str | None = None)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, message: str, slug: str | None = None) -> None:
    super().__init__(message)
    self.slug = slug

slug instance-attribute

Python
slug = slug

PromptVersionNotFoundException

Python
PromptVersionNotFoundException(slug: str, version: int)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, slug: str, version: int) -> None:
    super().__init__(f"Version {version} of prompt {slug!r} not found")
    self.slug = slug
    self.version = version

slug instance-attribute

Python
slug = slug

version instance-attribute

Python
version = version

RepositoryReadOnlyException

Python
RepositoryReadOnlyException(repository: str)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, repository: str) -> None:
    super().__init__(f"Repository {repository!r} is read-only")
    self.repository = repository

repository instance-attribute

Python
repository = repository

TemplateRenderException

Python
TemplateRenderException(message: str, slug: str | None = None, version: int | None = None)

Bases: PromptError

Source code in apogee_ai_prompt/domain/exceptions/prompt_exceptions.py
Python
def __init__(self, message: str, slug: str | None = None, version: int | None = None) -> None:
    super().__init__(message)
    self.slug = slug
    self.version = version

slug instance-attribute

Python
slug = slug

version instance-attribute

Python
version = version

Domain · Protocols (ports)

ICostEstimator

Bases: Protocol

name instance-attribute

Python
name: str

estimate

Python
estimate(*, provider: str, model: str, input_tokens: int, output_tokens: int) -> CostEstimate
Source code in apogee_ai_prompt/domain/services/i_cost_estimator.py
Python
def estimate(
    self,
    *,
    provider: str,
    model: str,
    input_tokens: int,
    output_tokens: int,
) -> CostEstimate:
    ...

IPromptLinter

Bases: Protocol

name instance-attribute

Python
name: str

lint

Python
lint(slug: str, version: PromptVersion) -> LintReport
Source code in apogee_ai_prompt/domain/services/i_prompt_linter.py
Python
def lint(self, slug: str, version: PromptVersion) -> LintReport:
    ...

IPromptRepository

Bases: Protocol

Combined Query + Command repository for Prompt aggregates.

Implementations may be read-only (GitPromptRepository reading from a fixed ref) — those raise RepositoryReadOnlyException on writes.

get async

Python
get(slug: str) -> Prompt

Return the Prompt aggregate by slug.

Raises:

Type Description
PromptNotFoundException

if no prompt exists with this slug.

Source code in apogee_ai_prompt/domain/repositories/i_prompt_repository.py
Python
async def get(self, slug: str) -> Prompt:
    """Return the Prompt aggregate by slug.

    Raises:
        PromptNotFoundException: if no prompt exists with this slug.
    """
    ...

find async

Python
find(slug: str) -> Prompt | None

Return the Prompt aggregate or None when not found.

Source code in apogee_ai_prompt/domain/repositories/i_prompt_repository.py
Python
async def find(self, slug: str) -> Prompt | None:
    """Return the Prompt aggregate or ``None`` when not found."""
    ...

list async

Python
list(*, tag: str | None = None, limit: int | None = None, offset: int = 0) -> list[Prompt]

List prompts matching the given filters.

Source code in apogee_ai_prompt/domain/repositories/i_prompt_repository.py
Python
async def list(
    self,
    *,
    tag: str | None = None,
    limit: int | None = None,
    offset: int = 0,
) -> list[Prompt]:
    """List prompts matching the given filters."""
    ...

save async

Python
save(prompt: Prompt) -> Prompt

Persist a Prompt aggregate (insert or update).

Source code in apogee_ai_prompt/domain/repositories/i_prompt_repository.py
Python
async def save(self, prompt: Prompt) -> Prompt:
    """Persist a Prompt aggregate (insert or update)."""
    ...

delete async

Python
delete(slug: str) -> None

Remove a Prompt aggregate.

Source code in apogee_ai_prompt/domain/repositories/i_prompt_repository.py
Python
async def delete(self, slug: str) -> None:
    """Remove a Prompt aggregate."""
    ...

exists async

Python
exists(slug: str) -> bool

Return True if a prompt with this slug exists.

Source code in apogee_ai_prompt/domain/repositories/i_prompt_repository.py
Python
async def exists(self, slug: str) -> bool:
    """Return True if a prompt with this slug exists."""
    ...

ITemplateEngine

Bases: Protocol

Render a template body against variables.

Implementations: Jinja2, Mustache (chevron), or a plain pass-through.

name instance-attribute

Python
name: str

render

Python
render(body: str, variables: dict[str, Any]) -> str

Render body. Raises TemplateRenderException on failure.

Source code in apogee_ai_prompt/domain/services/i_template_engine.py
Python
def render(self, body: str, variables: dict[str, Any]) -> str:
    """Render body. Raises TemplateRenderException on failure."""
    ...

discover_variables

Python
discover_variables(body: str) -> set[str]

Return the set of variable names referenced by the template body.

Source code in apogee_ai_prompt/domain/services/i_template_engine.py
Python
def discover_variables(self, body: str) -> set[str]:
    """Return the set of variable names referenced by the template body."""
    ...

ITokenCounter

Bases: Protocol

name instance-attribute

Python
name: str

count

Python
count(text: str, *, model: str | None = None) -> TokenCount
Source code in apogee_ai_prompt/domain/services/i_token_counter.py
Python
def count(self, text: str, *, model: str | None = None) -> TokenCount:
    ...

IVariantResolver

Bases: Protocol

name instance-attribute

Python
name: str

resolve

Python
resolve(config: CanaryConfig, *, routing_value: str) -> VariantSelection
Source code in apogee_ai_prompt/domain/services/i_variant_resolver.py
Python
def resolve(
    self,
    config: CanaryConfig,
    *,
    routing_value: str,
) -> VariantSelection:
    ...

Infrastructure

DefaultPromptLinter

Python
DefaultPromptLinter(*, min_chars: int = 10, max_chars: int = 16000)

12-rule linter for prompt versions.

Rules:

  1. empty-body (error): body must not be empty
  2. injection-pattern (warning): looks like a prompt-injection attempt
  3. too-long (warning): body length above max_chars
  4. too-short (info): body length below min_chars
  5. unused-variable (warning): declared but not referenced
  6. undeclared-variable (error): referenced in body but missing in schema
  7. required-with-default (info): required=True but a default is set
  8. mixed-engines (warning): body looks Jinja but engine is plain/mustache
  9. trailing-whitespace (info): trailing whitespace lines
  10. triple-blank-line (info): three or more consecutive blank lines
  11. no-newline-at-end (info): missing terminal newline
  12. no-system-prologue (info): chat prompt without leading role-setting line
Source code in apogee_ai_prompt/infrastructure/linters/default_prompt_linter.py
Python
def __init__(self, *, min_chars: int = 10, max_chars: int = 16000) -> None:
    self._min_chars = min_chars
    self._max_chars = max_chars
    self._jinja = JinjaTemplateEngine()
    self._plain = PlainTemplateEngine()

name class-attribute instance-attribute

Python
name = 'default'

lint

Python
lint(slug: str, version: PromptVersion) -> LintReport
Source code in apogee_ai_prompt/infrastructure/linters/default_prompt_linter.py
Python
def lint(self, slug: str, version: PromptVersion) -> LintReport:
    issues: list[LintIssue] = []
    body = version.body

    # 1. empty-body
    if not body or not body.strip():
        issues.append(
            LintIssue("empty-body", LintSeverity.ERROR, "Prompt body is empty")
        )

    # 2. injection-pattern
    for pattern, rule_id in _INJECTION_PATTERNS:
        for match in re.finditer(pattern, body):
            issues.append(
                LintIssue(
                    f"injection-pattern:{rule_id}",
                    LintSeverity.WARNING,
                    f"Possible prompt injection pattern: {match.group(0)!r}",
                )
            )

    # 3 / 4. length
    if len(body) > self._max_chars:
        issues.append(
            LintIssue(
                "too-long",
                LintSeverity.WARNING,
                f"Body has {len(body)} chars (>{self._max_chars})",
            )
        )
    if 0 < len(body.strip()) < self._min_chars:
        issues.append(
            LintIssue(
                "too-short",
                LintSeverity.INFO,
                f"Body has only {len(body.strip())} chars (<{self._min_chars})",
            )
        )

    # 5 / 6. variable consistency
    try:
        referenced = self._discover_for(version, body)
    except Exception:  # noqa: BLE001 - linter should never crash on bad input
        referenced = set()
    declared = {v.name for v in version.variables}
    for name in sorted(declared - referenced):
        issues.append(
            LintIssue(
                "unused-variable",
                LintSeverity.WARNING,
                f"Variable {name!r} declared but not used in body",
            )
        )
    for name in sorted(referenced - declared):
        issues.append(
            LintIssue(
                "undeclared-variable",
                LintSeverity.ERROR,
                f"Variable {name!r} used in body but not declared in schema",
            )
        )

    # 7. required-with-default
    for var in version.variables:
        if var.required and var.default is not None:
            issues.append(
                LintIssue(
                    "required-with-default",
                    LintSeverity.INFO,
                    f"Variable {var.name!r} is required but also has a default",
                )
            )

    # 8. mixed-engines
    if version.engine != TemplateEngineKind.JINJA and re.search(r"\{\{.*?\}\}", body):
        issues.append(
            LintIssue(
                "mixed-engines",
                LintSeverity.WARNING,
                f"Body uses Jinja-like {{{{ ... }}}} but engine is {version.engine.value}",
            )
        )

    # 9. trailing whitespace
    for i, line in enumerate(body.splitlines(), start=1):
        if line and line != line.rstrip():
            issues.append(
                LintIssue(
                    "trailing-whitespace",
                    LintSeverity.INFO,
                    "Trailing whitespace",
                    line=i,
                )
            )
            break

    # 10. triple-blank-line
    if "\n\n\n" in body:
        issues.append(
            LintIssue(
                "triple-blank-line",
                LintSeverity.INFO,
                "Three or more consecutive blank lines",
            )
        )

    # 11. no-newline-at-end
    if body and not body.endswith("\n"):
        issues.append(
            LintIssue(
                "no-newline-at-end",
                LintSeverity.INFO,
                "Body does not end with a newline",
            )
        )

    # 12. no-system-prologue (only for chat prompts)
    from ...domain.enums.prompt_kind_enum import PromptKind

    kind_str = getattr(version, "kind", None)
    del kind_str
    # The version itself has no kind — chat-vs-other is at Prompt level.
    # We still surface this as an INFO since it is a soft style suggestion
    # only when the body is short and lacks a leading role definition.
    first_line = body.strip().splitlines()[0].lower() if body.strip() else ""
    if (
        first_line
        and len(body.strip()) < 200
        and not re.match(
            r"^(you are|act as|behave as|role:|system:)",
            first_line,
        )
    ):
        issues.append(
            LintIssue(
                "no-system-prologue",
                LintSeverity.INFO,
                "Short prompt missing role-setting prologue ('You are ...').",
            )
        )

    # Reference PromptKind to keep the import meaningful if reused.
    _ = PromptKind

    return LintReport(slug=slug, version=version.version, issues=tuple(issues))

FilePromptRepository

Python
FilePromptRepository(root: str | Path)

YAML-on-disk repository: one <root>/<slug>.yml file per Prompt.

Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
def __init__(self, root: str | Path) -> None:
    self._root = Path(root)

get async

Python
get(slug: str) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
async def get(self, slug: str) -> Prompt:
    prompt = await self.find(slug)
    if prompt is None:
        raise PromptNotFoundException(slug)
    return prompt

find async

Python
find(slug: str) -> Prompt | None
Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
async def find(self, slug: str) -> Prompt | None:
    return await asyncio.to_thread(self._read_one, slug)

list async

Python
list(*, tag: str | None = None, limit: int | None = None, offset: int = 0) -> list[Prompt]
Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
async def list(
    self,
    *,
    tag: str | None = None,
    limit: int | None = None,
    offset: int = 0,
) -> list[Prompt]:
    items = await asyncio.to_thread(self._read_all)
    if tag is not None:
        items = [p for p in items if tag in p.tags]
    items.sort(key=lambda p: p.slug)
    if offset:
        items = items[offset:]
    if limit is not None:
        items = items[:limit]
    return items

save async

Python
save(prompt: Prompt) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
async def save(self, prompt: Prompt) -> Prompt:
    await asyncio.to_thread(self._write_one, prompt)
    return prompt

delete async

Python
delete(slug: str) -> None
Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
async def delete(self, slug: str) -> None:
    await asyncio.to_thread(self._delete_one, slug)

exists async

Python
exists(slug: str) -> bool
Source code in apogee_ai_prompt/infrastructure/repositories/file_prompt_repository.py
Python
async def exists(self, slug: str) -> bool:
    return await asyncio.to_thread(self._path(slug).is_file)

GitPromptRepository

Python
GitPromptRepository(repo_path: str | Path, *, ref: str = 'HEAD', prompts_dir: str = 'prompts')

Read-only repository that loads prompts from a fixed git ref.

Powered by dulwich — pure-Python git, so no system git binary is required. Useful when prompt evolution must be auditable: production pins a tag (v1.4.0) and any change requires a code review.

Writes raise RepositoryReadOnlyException.

Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
def __init__(
    self,
    repo_path: str | Path,
    *,
    ref: str = "HEAD",
    prompts_dir: str = "prompts",
) -> None:
    try:
        from dulwich import porcelain  # type: ignore  # noqa: F401
    except ImportError as exc:
        raise ImportError(
            "GitPromptRepository requires `dulwich`. "
            "Install with: pip install 'apogee-ai-prompt[git]'"
        ) from exc
    self._repo_path = Path(repo_path)
    self._ref = ref
    self._prompts_dir = prompts_dir.strip("/")

name class-attribute instance-attribute

Python
name = 'git'

get async

Python
get(slug: str) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
async def get(self, slug: str) -> Prompt:
    prompt = await self.find(slug)
    if prompt is None:
        raise PromptNotFoundException(slug)
    return prompt

find async

Python
find(slug: str) -> Prompt | None
Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
async def find(self, slug: str) -> Prompt | None:
    return await asyncio.to_thread(self._read_one, slug)

list async

Python
list(*, tag: str | None = None, limit: int | None = None, offset: int = 0) -> list[Prompt]
Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
async def list(
    self,
    *,
    tag: str | None = None,
    limit: int | None = None,
    offset: int = 0,
) -> list[Prompt]:
    items = await asyncio.to_thread(self._read_all)
    if tag is not None:
        items = [p for p in items if tag in p.tags]
    items.sort(key=lambda p: p.slug)
    if offset:
        items = items[offset:]
    if limit is not None:
        items = items[:limit]
    return items

save async

Python
save(prompt: Prompt) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
async def save(self, prompt: Prompt) -> Prompt:
    raise RepositoryReadOnlyException(self.name)

delete async

Python
delete(slug: str) -> None
Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
async def delete(self, slug: str) -> None:
    raise RepositoryReadOnlyException(self.name)

exists async

Python
exists(slug: str) -> bool
Source code in apogee_ai_prompt/infrastructure/repositories/git_prompt_repository.py
Python
async def exists(self, slug: str) -> bool:
    return await asyncio.to_thread(self._read_one, slug) is not None

HeuristicTokenCounter

Approximates token count as ceil(len(text)/4).

Decent for English-like text; not a substitute for tiktoken when accurate counts matter (use TiktokenTokenCounter instead).

name class-attribute instance-attribute

Python
name = 'heuristic'

count

Python
count(text: str, *, model: str | None = None) -> TokenCount
Source code in apogee_ai_prompt/infrastructure/token_counters/heuristic_token_counter.py
Python
def count(self, text: str, *, model: str | None = None) -> TokenCount:
    if not text:
        return TokenCount(tokens=0, counter=self.name)
    # +3 / 4 trick = ceil division
    return TokenCount(tokens=(len(text) + 3) // 4, counter=self.name)

InMemoryPromptRepository

Python
InMemoryPromptRepository()

Thread-unsafe in-process implementation; ideal for tests and dev runs.

Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
def __init__(self) -> None:
    self._store: dict[str, Prompt] = {}

get async

Python
get(slug: str) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
async def get(self, slug: str) -> Prompt:
    if slug not in self._store:
        raise PromptNotFoundException(slug)
    return self._store[slug]

find async

Python
find(slug: str) -> Prompt | None
Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
async def find(self, slug: str) -> Prompt | None:
    return self._store.get(slug)

list async

Python
list(*, tag: str | None = None, limit: int | None = None, offset: int = 0) -> list[Prompt]
Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
async def list(
    self,
    *,
    tag: str | None = None,
    limit: int | None = None,
    offset: int = 0,
) -> list[Prompt]:
    items = list(self._store.values())
    if tag is not None:
        items = [p for p in items if tag in p.tags]
    items.sort(key=lambda p: p.slug)
    if offset:
        items = items[offset:]
    if limit is not None:
        items = items[:limit]
    return items

save async

Python
save(prompt: Prompt) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
async def save(self, prompt: Prompt) -> Prompt:
    self._store[prompt.slug] = deepcopy(prompt)
    return self._store[prompt.slug]

delete async

Python
delete(slug: str) -> None
Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
async def delete(self, slug: str) -> None:
    if slug in self._store:
        del self._store[slug]

exists async

Python
exists(slug: str) -> bool
Source code in apogee_ai_prompt/infrastructure/repositories/in_memory_prompt_repository.py
Python
async def exists(self, slug: str) -> bool:
    return slug in self._store

JinjaTemplateEngine

Python
JinjaTemplateEngine()
Source code in apogee_ai_prompt/infrastructure/template_engines/jinja_template_engine.py
Python
def __init__(self) -> None:
    self._env = Environment(
        undefined=StrictUndefined,
        keep_trailing_newline=True,
        autoescape=False,
    )

name class-attribute instance-attribute

Python
name = 'jinja'

render

Python
render(body: str, variables: dict[str, Any]) -> str
Source code in apogee_ai_prompt/infrastructure/template_engines/jinja_template_engine.py
Python
def render(self, body: str, variables: dict[str, Any]) -> str:
    try:
        template = self._env.from_string(body)
        return template.render(**variables)
    except UndefinedError as exc:
        raise TemplateRenderException(f"Undefined variable in Jinja template: {exc}") from exc
    except TemplateError as exc:
        raise TemplateRenderException(f"Jinja render error: {exc}") from exc

discover_variables

Python
discover_variables(body: str) -> set[str]
Source code in apogee_ai_prompt/infrastructure/template_engines/jinja_template_engine.py
Python
def discover_variables(self, body: str) -> set[str]:
    try:
        ast = self._env.parse(body)
        return set(meta.find_undeclared_variables(ast))
    except TemplateError:
        return set(_JINJA_VAR_RE.findall(body))

PRICING_TABLE module-attribute

Python
PRICING_TABLE: dict[str, dict[str, tuple[float, float]]] = {'openai': {'gpt-4o': (5.0, 15.0), 'gpt-4o-mini': (0.15, 0.6), 'gpt-4-turbo': (10.0, 30.0), 'gpt-4': (30.0, 60.0), 'gpt-3.5-turbo': (0.5, 1.5), 'o1': (15.0, 60.0), 'o1-mini': (3.0, 12.0), 'o3-mini': (1.1, 4.4), 'text-embedding-3-large': (0.13, 0.0), 'text-embedding-3-small': (0.02, 0.0)}, 'anthropic': {'claude-3-5-sonnet': (3.0, 15.0), 'claude-3-5-haiku': (0.8, 4.0), 'claude-3-opus': (15.0, 75.0), 'claude-3-sonnet': (3.0, 15.0), 'claude-3-haiku': (0.25, 1.25), 'claude-opus-4': (15.0, 75.0), 'claude-sonnet-4': (3.0, 15.0), 'claude-haiku-4': (0.8, 4.0)}, 'google': {'gemini-1.5-pro': (1.25, 5.0), 'gemini-1.5-flash': (0.075, 0.3), 'gemini-2.0-flash': (0.1, 0.4), 'gemini-2.5-pro': (1.25, 5.0), 'gemini-2.5-flash': (0.1, 0.4)}, 'openrouter': {'default': (1.0, 3.0)}, 'bedrock': {'anthropic.claude-3-5-sonnet': (3.0, 15.0), 'anthropic.claude-3-haiku': (0.25, 1.25), 'amazon.titan-text-express': (0.2, 0.6), 'meta.llama3-70b-instruct': (2.65, 3.5)}}

PlainTemplateEngine

Python str.format-style placeholder renderer with no extras.

Useful when neither Jinja nor Mustache is desired and you just want {name} substitution.

name class-attribute instance-attribute

Python
name = 'plain'

render

Python
render(body: str, variables: dict[str, Any]) -> str
Source code in apogee_ai_prompt/infrastructure/template_engines/plain_template_engine.py
Python
def render(self, body: str, variables: dict[str, Any]) -> str:
    try:
        return body.format(**variables)
    except KeyError as exc:
        raise TemplateRenderException(f"Missing variable in plain template: {exc}") from exc
    except (IndexError, ValueError) as exc:
        raise TemplateRenderException(f"Plain render error: {exc}") from exc

discover_variables

Python
discover_variables(body: str) -> set[str]
Source code in apogee_ai_prompt/infrastructure/template_engines/plain_template_engine.py
Python
def discover_variables(self, body: str) -> set[str]:
    names: set[str] = set()
    for _, field_name, _, _ in Formatter().parse(body):
        if field_name:
            names.add(field_name.split(".", 1)[0].split("[", 1)[0])
    return names

SqlPromptRepository

Python
SqlPromptRepository(session_factory: Any, *, table_name: str = 'apogee_prompts')

SQLAlchemy 2.x async repository.

The schema is intentionally simple: one row per Prompt with a JSON column storing the serialized aggregate. Lower friction for migrations than splitting versions into a separate table; trade-off is querying by version across prompts requires JSON path operators (Postgres) or scanning.

SQLAlchemy is imported lazily so installing apogee-ai-prompt without the [sql] extra still works.

Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
def __init__(self, session_factory: Any, *, table_name: str = "apogee_prompts") -> None:
    try:
        import sqlalchemy  # type: ignore  # noqa: F401
    except ImportError as exc:  # pragma: no cover - exercised by extras
        raise ImportError(
            "SqlPromptRepository requires SQLAlchemy. "
            "Install with: pip install 'apogee-ai-prompt[sql]'"
        ) from exc
    self._session_factory = session_factory
    self._table_name = table_name
    self._table = self._build_table()

name class-attribute instance-attribute

Python
name = 'sql'

ensure_schema async

Python
ensure_schema(engine: Any) -> None
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def ensure_schema(self, engine: Any) -> None:
    async with engine.begin() as conn:
        await conn.run_sync(self._table.metadata.create_all)

get async

Python
get(slug: str) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def get(self, slug: str) -> Prompt:
    prompt = await self.find(slug)
    if prompt is None:
        raise PromptNotFoundException(slug)
    return prompt

find async

Python
find(slug: str) -> Prompt | None
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def find(self, slug: str) -> Prompt | None:
    from sqlalchemy import select

    async with self._session_factory() as session:
        stmt = select(self._table.c.data).where(self._table.c.slug == slug)
        row = (await session.execute(stmt)).first()
        if row is None:
            return None
        data = self._decode(row[0])
        return prompt_from_dict(data)

list async

Python
list(*, tag: str | None = None, limit: int | None = None, offset: int = 0) -> list[Prompt]
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def list(
    self,
    *,
    tag: str | None = None,
    limit: int | None = None,
    offset: int = 0,
) -> list[Prompt]:
    from sqlalchemy import select

    async with self._session_factory() as session:
        stmt = select(self._table.c.data).order_by(self._table.c.slug)
        rows = (await session.execute(stmt)).all()
    items = [prompt_from_dict(self._decode(r[0])) for r in rows]
    if tag is not None:
        items = [p for p in items if tag in p.tags]
    if offset:
        items = items[offset:]
    if limit is not None:
        items = items[:limit]
    return items

save async

Python
save(prompt: Prompt) -> Prompt
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def save(self, prompt: Prompt) -> Prompt:
    from sqlalchemy.dialects.sqlite import insert as sqlite_insert
    from sqlalchemy.exc import CompileError

    payload = prompt_to_dict(prompt)
    async with self._session_factory() as session:
        try:
            stmt = sqlite_insert(self._table).values(slug=prompt.slug, data=payload)
            stmt = stmt.on_conflict_do_update(
                index_elements=[self._table.c.slug],
                set_={"data": payload},
            )
            await session.execute(stmt)
        except (CompileError, NotImplementedError):
            # Fallback: delete + insert (works on any backend)
            from sqlalchemy import delete, insert

            await session.execute(delete(self._table).where(self._table.c.slug == prompt.slug))
            await session.execute(
                insert(self._table).values(slug=prompt.slug, data=payload)
            )
        await session.commit()
    return prompt

delete async

Python
delete(slug: str) -> None
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def delete(self, slug: str) -> None:
    from sqlalchemy import delete

    async with self._session_factory() as session:
        await session.execute(delete(self._table).where(self._table.c.slug == slug))
        await session.commit()

exists async

Python
exists(slug: str) -> bool
Source code in apogee_ai_prompt/infrastructure/repositories/sql_prompt_repository.py
Python
async def exists(self, slug: str) -> bool:
    from sqlalchemy import select

    async with self._session_factory() as session:
        stmt = select(self._table.c.slug).where(self._table.c.slug == slug)
        return (await session.execute(stmt)).first() is not None

TableCostEstimator

Python
TableCostEstimator(pricing: dict[str, dict[str, tuple[float, float]]] | None = None)

Cost estimator backed by a static pricing table.

Pricing is expressed per 1M tokens. Override or extend the bundled PRICING_TABLE by passing a custom dict.

Source code in apogee_ai_prompt/infrastructure/cost_estimators/table_cost_estimator.py
Python
def __init__(self, pricing: dict[str, dict[str, tuple[float, float]]] | None = None) -> None:
    self._pricing = pricing if pricing is not None else PRICING_TABLE

name class-attribute instance-attribute

Python
name = 'table'

estimate

Python
estimate(*, provider: str, model: str, input_tokens: int, output_tokens: int) -> CostEstimate
Source code in apogee_ai_prompt/infrastructure/cost_estimators/table_cost_estimator.py
Python
def estimate(
    self,
    *,
    provider: str,
    model: str,
    input_tokens: int,
    output_tokens: int,
) -> CostEstimate:
    price = lookup_price(provider, model) if self._pricing is PRICING_TABLE else self._lookup(
        provider, model
    )
    if price is None:
        input_per_m, output_per_m = 0.0, 0.0
    else:
        input_per_m, output_per_m = price
    input_cost = (input_tokens / 1_000_000.0) * input_per_m
    output_cost = (output_tokens / 1_000_000.0) * output_per_m
    return CostEstimate(
        provider=provider,
        model=model,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
        input_cost_usd=round(input_cost, 6),
        output_cost_usd=round(output_cost, 6),
    )

TiktokenTokenCounter

Python
TiktokenTokenCounter(*, default_encoding: str = 'cl100k_base')

Accurate counter backed by OpenAI's tiktoken.

Lazy import: only required if you install apogee-ai-prompt[tiktoken].

Source code in apogee_ai_prompt/infrastructure/token_counters/tiktoken_token_counter.py
Python
def __init__(self, *, default_encoding: str = "cl100k_base") -> None:
    try:
        import tiktoken  # type: ignore  # noqa: F401
    except ImportError as exc:
        raise ImportError(
            "TiktokenTokenCounter requires `tiktoken`. "
            "Install with: pip install 'apogee-ai-prompt[tiktoken]'"
        ) from exc
    self._default_encoding = default_encoding

name class-attribute instance-attribute

Python
name = 'tiktoken'

count

Python
count(text: str, *, model: str | None = None) -> TokenCount
Source code in apogee_ai_prompt/infrastructure/token_counters/tiktoken_token_counter.py
Python
def count(self, text: str, *, model: str | None = None) -> TokenCount:
    import tiktoken  # type: ignore

    if model:
        try:
            enc = tiktoken.encoding_for_model(model)
        except (KeyError, ValueError):
            enc = tiktoken.get_encoding(self._default_encoding)
    else:
        enc = tiktoken.get_encoding(self._default_encoding)
    return TokenCount(tokens=len(enc.encode(text)), counter=self.name)

WeightedHashResolver

Deterministic A/B + canary resolver.

Computes bucket = hash(slug:routing_value) / 2^64 (uniform in [0,1)) and walks the variants in ascending version order, accumulating weights. The first cumulative weight greater than the bucket wins. Same routing value always lands on the same version, which is what makes this safe for persistent A/B testing.

name class-attribute instance-attribute

Python
name = 'weighted-hash'

resolve

Python
resolve(config: CanaryConfig, *, routing_value: str) -> VariantSelection
Source code in apogee_ai_prompt/infrastructure/resolvers/weighted_hash_resolver.py
Python
def resolve(
    self,
    config: CanaryConfig,
    *,
    routing_value: str,
) -> VariantSelection:
    digest = hashlib.sha256(f"{config.slug}:{routing_value}".encode()).digest()
    # Use first 8 bytes as unsigned 64-bit integer
    as_int = int.from_bytes(digest[:8], "big")
    bucket = as_int / float(1 << 64)

    cumulative = 0.0
    for version in sorted(config.variants):
        cumulative += config.variants[version]
        if bucket < cumulative:
            return VariantSelection(
                slug=config.slug,
                selected_version=version,
                routing_key=config.routing_key,
                routing_value=routing_value,
                bucket=bucket,
            )
    # Fallback (rounding edge case): pick the last version
    last = max(config.variants)
    return VariantSelection(
        slug=config.slug,
        selected_version=last,
        routing_key=config.routing_key,
        routing_value=routing_value,
        bucket=bucket,
    )

default_engines

Python
default_engines(*, include_mustache: bool = False) -> dict[TemplateEngineKind, ITemplateEngine]

Return a registry of template engines suitable for RenderPromptUseCase.

Mustache is opt-in because chevron is an optional dependency.

Source code in apogee_ai_prompt/infrastructure/template_engines/registry.py
Python
def default_engines(*, include_mustache: bool = False) -> dict[TemplateEngineKind, ITemplateEngine]:
    """Return a registry of template engines suitable for ``RenderPromptUseCase``.

    Mustache is opt-in because ``chevron`` is an optional dependency.
    """

    engines: dict[TemplateEngineKind, ITemplateEngine] = {
        TemplateEngineKind.JINJA: JinjaTemplateEngine(),
        TemplateEngineKind.PLAIN: PlainTemplateEngine(),
    }
    if include_mustache:
        from .mustache_template_engine import MustacheTemplateEngine

        engines[TemplateEngineKind.MUSTACHE] = MustacheTemplateEngine()
    return engines

lookup_price

Python
lookup_price(provider: str, model: str) -> tuple[float, float] | None

Return (price_input, price_output) per 1M tokens or None.

The lookup is case-insensitive. If the exact model is not registered the provider's default entry (when present) is used.

Source code in apogee_ai_prompt/infrastructure/cost_estimators/pricing_table.py
Python
def lookup_price(provider: str, model: str) -> tuple[float, float] | None:
    """Return ``(price_input, price_output)`` per 1M tokens or ``None``.

    The lookup is case-insensitive. If the exact model is not registered the
    provider's ``default`` entry (when present) is used.
    """
    p = provider.lower()
    if p not in PRICING_TABLE:
        return None
    catalog = PRICING_TABLE[p]
    m = model.lower()
    if m in catalog:
        return catalog[m]
    # Substring fallback (e.g., "claude-3-5-sonnet-20241022" → "claude-3-5-sonnet")
    for key in catalog:
        if key in m:
            return catalog[key]
    if "default" in catalog:
        return catalog["default"]
    return None