File size: 3,042 Bytes
0bb4dfa
 
 
a763505
4944128
a763505
 
 
0bb4dfa
 
 
 
 
 
 
 
 
 
a763505
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a763505
 
0bb4dfa
a763505
0bb4dfa
 
 
 
 
 
d58dc6d
a763505
 
 
 
0bb4dfa
d58dc6d
0bb4dfa
 
a763505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d58dc6d
a763505
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
"""Participant catalog API.

`GET /api/personas` returns three sections - Neon HANA personas (with
vanilla/RAG personas filtered out and display names reformatted via an
LLM), the bundled extra personas, and the user-supplied expert
personas (which are local-only on the frontend but echoed here for
completeness so the API can act as the source of truth for participant
choices when needed).
"""
from __future__ import annotations

import logging

from fastapi import APIRouter
from fastapi.responses import JSONResponse

from app.clients.hana_client import hana_client
from app.services.extra_personas import list_extra_personas
from app.services.persona_naming import reformat_neon_names

router = APIRouter()
LOG = logging.getLogger(__name__)


def _is_vanilla_or_rag(persona_name: str) -> bool:
    pn = (persona_name or "").lower()
    return "vanilla" in pn or "rag" in pn


@router.get("/personas")
async def get_personas():
    """Return the participant catalog the frontend dropdown shows."""
    try:
        neon_models = await hana_client.get_models()
    except Exception as exc:
        LOG.warning("HANA models unavailable: %s", exc)
        neon_models = []

    # Pass 1: collect raw (model_short, persona_name) pairs.
    raw_neon: list[dict] = []
    for nm in neon_models or []:
        model_short = nm["name"].split("/")[-1]
        for p in nm.get("personas", []) or []:
            if p.get("enabled") is False:
                continue
            persona_name = p.get("persona_name") or ""
            if _is_vanilla_or_rag(persona_name):
                continue
            system_prompt = (p.get("system_prompt") or "").strip()
            raw_neon.append({
                "model_id": nm["model_id"],
                "model_short": model_short,
                "persona_name": persona_name,
                "description": p.get("description") or "",
                "role_prompt": system_prompt,
            })

    # Pass 2: batch-reformat display names via the orchestrator LLM
    # (cached). One call covers every uncached pair.
    pairs = [(r["model_short"], r["persona_name"]) for r in raw_neon]
    name_map = await reformat_neon_names(pairs)

    neon_personas = []
    for r in raw_neon:
        display = name_map.get(
            (r["model_short"], r["persona_name"]),
            r["persona_name"],
        )
        participant_id = f"neon:{r['model_id']}:{r['persona_name']}"
        neon_personas.append({
            "participant_id": participant_id,
            "kind": "neon",
            "name": display,
            "model_display": r["model_short"],
            "default_model_id": participant_id,
            "description": r["description"],
            "role_prompt": r.get("role_prompt") or "",
        })

    extras = list_extra_personas()
    for e in extras:
        e["model_display"] = e["default_model_id"]

    return JSONResponse(
        content={
            "neon": neon_personas,
            "extra": extras,
        },
        headers={"Cache-Control": "no-store"},
    )