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
20 changes: 20 additions & 0 deletions prompt_forge/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,23 @@ class ModelEffectivenessResponse(BaseModel):
economy: EffectivenessSummary | None = None
standard: EffectivenessSummary | None = None
premium: EffectivenessSummary | None = None


# --- Persona Prompts ---


class PersonaPromptCreate(BaseModel):
"""Create a new persona prompt version."""

template: str = Field(..., min_length=1)


class PersonaPromptResponse(BaseModel):
"""Persona prompt response."""

id: UUID
persona: str
version: int
template: str
is_latest: bool
created_at: datetime
78 changes: 78 additions & 0 deletions prompt_forge/api/persona_prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Persona prompt versioning API endpoints."""

from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException

from prompt_forge.api.models import PersonaPromptCreate, PersonaPromptResponse
from prompt_forge.db.persona_store import PersonaPromptStore, get_persona_store

router = APIRouter()


@router.get("/{persona}", response_model=PersonaPromptResponse)
async def get_persona_prompt_latest(
persona: str,
store: PersonaPromptStore = Depends(get_persona_store),
) -> PersonaPromptResponse:
"""Get the latest version of a persona prompt."""
prompt = store.get_latest_persona_prompt(persona)
if not prompt:
raise HTTPException(status_code=404, detail=f"Persona '{persona}' not found")

return PersonaPromptResponse(**prompt.model_dump())


@router.post("/seed", status_code=201)
async def seed_initial_personas(
store: PersonaPromptStore = Depends(get_persona_store),
) -> dict[str, str]:
"""Seed initial personas with basic templates."""
try:
store.seed_initial_personas()
return {"message": "Initial personas seeded successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to seed personas: {str(e)}")


@router.get("/{persona}/versions", response_model=list[PersonaPromptResponse])
async def list_persona_prompt_versions(
persona: str,
store: PersonaPromptStore = Depends(get_persona_store),
) -> list[PersonaPromptResponse]:
"""List all versions of a persona prompt."""
prompts = store.list_persona_versions(persona)
if not prompts:
raise HTTPException(status_code=404, detail=f"Persona '{persona}' not found")

return [PersonaPromptResponse(**prompt.model_dump()) for prompt in prompts]


@router.post("/{persona}", response_model=PersonaPromptResponse, status_code=201)
async def create_persona_prompt_version(
persona: str,
data: PersonaPromptCreate,
store: PersonaPromptStore = Depends(get_persona_store),
) -> PersonaPromptResponse:
"""Create a new version of a persona prompt (auto-increments version, sets previous is_latest=false)."""
try:
prompt = store.create_persona_prompt_version(persona, data.template)
return PersonaPromptResponse(**prompt.model_dump())
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create persona prompt: {str(e)}")


@router.get("/{persona}/{version}", response_model=PersonaPromptResponse)
async def get_persona_prompt_version(
persona: str,
version: int,
store: PersonaPromptStore = Depends(get_persona_store),
) -> PersonaPromptResponse:
"""Get a specific version of a persona prompt."""
prompt = store.get_persona_prompt_version(persona, version)
if not prompt:
raise HTTPException(
status_code=404, detail=f"Persona '{persona}' version {version} not found"
)

return PersonaPromptResponse(**prompt.model_dump())
87 changes: 87 additions & 0 deletions prompt_forge/api/personas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Persona convenience endpoints — thin wrappers over the prompt resolver."""

from __future__ import annotations

import os
from typing import Any

import httpx
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query

from prompt_forge.api.models import VersionResponse
from prompt_forge.core.resolver import PromptResolver, get_resolver

logger = structlog.get_logger()

router = APIRouter()

ALEXANDRIA_URL = os.getenv("ALEXANDRIA_URL", "")


@router.get("/{persona}", response_model=VersionResponse)
async def get_persona(
persona: str,
branch: str = Query("main"),
strategy: str = Query("latest"),
resolver: PromptResolver = Depends(get_resolver),
) -> VersionResponse:
"""Get the latest version of a persona prompt."""
try:
version = resolver.resolve(slug=persona, branch=branch, strategy=strategy)
return VersionResponse(**version)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))


@router.get("/{persona}/{version}", response_model=VersionResponse)
async def get_persona_version(
persona: str,
version: int,
branch: str = Query("main"),
resolver: PromptResolver = Depends(get_resolver),
) -> VersionResponse:
"""Get a specific version of a persona prompt."""
try:
result = resolver.resolve(slug=persona, branch=branch, version=version, strategy="pinned")
return VersionResponse(**result)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))


@router.get("/{persona}/effective")
async def get_persona_effective(
persona: str,
branch: str = Query("main"),
strategy: str = Query("latest"),
resolver: PromptResolver = Depends(get_resolver),
) -> dict[str, Any]:
"""Get latest persona version merged with Alexandria context (if configured)."""
try:
version = resolver.resolve(slug=persona, branch=branch, strategy=strategy)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))

result: dict[str, Any] = {
"version": VersionResponse(**version).model_dump(mode="json"),
"alexandria_context": None,
}

if ALEXANDRIA_URL:
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(
f"{ALEXANDRIA_URL}/api/v1/context/{persona}",
)
if resp.status_code == 200:
result["alexandria_context"] = resp.json()
else:
logger.warning(
"personas.alexandria_unavailable",
persona=persona,
status=resp.status_code,
)
except httpx.HTTPError as exc:
logger.warning("personas.alexandria_error", persona=persona, error=str(exc))

return result
4 changes: 4 additions & 0 deletions prompt_forge/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from prompt_forge.api.branches import router as branches_router
from prompt_forge.api.compose import router as compose_router
from prompt_forge.api.effectiveness import router as effectiveness_router
from prompt_forge.api.persona_prompts import router as persona_prompts_router
from prompt_forge.api.prompts import router as prompts_router
from prompt_forge.api.scan import router as scan_router
from prompt_forge.api.subscriptions import router as subscriptions_router
Expand All @@ -20,6 +21,9 @@
api_router.include_router(versions_router, prefix="/prompts", tags=["versions"])
api_router.include_router(branches_router, prefix="/prompts", tags=["branches"])
api_router.include_router(subscriptions_router, prefix="/prompts", tags=["subscriptions"])
api_router.include_router(
persona_prompts_router, prefix="/persona-prompts", tags=["persona-prompts"]
)
api_router.include_router(agents_router, prefix="/agents", tags=["agents"])
api_router.include_router(compose_router, tags=["composition"])
api_router.include_router(usage_router, prefix="/usage", tags=["usage"])
Expand Down
11 changes: 11 additions & 0 deletions prompt_forge/db/migrations/007_persona_prompts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE persona_prompts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
persona TEXT NOT NULL,
version INT NOT NULL DEFAULT 1,
template TEXT NOT NULL,
is_latest BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(persona, version)
);

CREATE INDEX idx_persona_prompts_latest ON persona_prompts(persona, is_latest) WHERE is_latest = TRUE;
11 changes: 11 additions & 0 deletions prompt_forge/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,14 @@ class UsageLogRow(BaseModel):
outcome: str
latency_ms: int | None
feedback: dict[str, Any] | None


class PersonaPromptRow(BaseModel):
"""Row from the persona_prompts table."""

id: UUID
persona: str
version: int
template: str
is_latest: bool
created_at: datetime
Loading
Loading