Files
sales-trainer/backend/app/api/me_routes.py

192 lines
6.4 KiB
Python

"""Trainee routes: win/lose board, weak-areas, generate own persona."""
from __future__ import annotations
import uuid
from flask import Blueprint, jsonify, request
from ..llm import LLMError
from ..services.trainee import MyPersonaStore, analyze_weak_areas
from .helpers import (
ApiError,
current_user,
internal_error,
is_valid_tenant_id,
request_json_object,
require_auth,
require_roles,
)
me_bp = Blueprint("me", __name__)
def _stores():
from flask import current_app
return {
"groups": current_app.extensions["group_store"],
"sessions": current_app.extensions["session_store"],
"my_personas": current_app.extensions.get("my_persona_store"),
"llm": current_app.extensions["llm"],
}
def _authorized_finished_trainee_sessions(s, actor) -> list[dict]:
"""Return only current, actor-authorized sessions usable as evidence."""
org_id = actor.get("org_id")
user_id = actor.get("id")
if not is_valid_tenant_id(org_id) or not isinstance(user_id, str) or not user_id:
raise ApiError("permission denied", 403)
sessions = s["sessions"].list_for_user(user_id, org_id=org_id)
from .chat_routes import _authorize_session_context
authorized = []
for session in sessions:
if (
not isinstance(session, dict)
or (session.get("mode") or "trainee") != "trainee"
or session.get("status") != "finished"
or session.get("outcome") not in ("won", "lost")
):
continue
try:
_authorize_session_context(s, session, required_status="finished")
except ApiError:
continue
authorized.append(session)
return authorized
@me_bp.get("/board")
@require_auth
def win_lose_board():
"""Per-persona won/lost/not-tried across all groups the user sees (any authenticated user)."""
s = _stores()
actor = current_user()
uid = actor["id"]
org_id = actor.get("org_id")
if not is_valid_tenant_id(org_id):
raise ApiError("permission denied", 403)
my_sessions = s["sessions"].list_for_user(uid, org_id=org_id)
outcome_by = {
(x.get("group_id"), x.get("persona_id")): (
"active" if x.get("status") == "active" else x.get("outcome")
)
for x in my_sessions
if isinstance(x, dict)
}
groups = s["groups"].list_visible_to(
role="user", org_id=org_id, user_id=uid
)
items = []
for g in groups:
raw_personas = g.get("personas")
personas = raw_personas if isinstance(raw_personas, list) else []
for p in personas:
if not isinstance(p, dict):
continue
group_id = g.get("id")
persona_id = p.get("id")
if not isinstance(group_id, str) or not group_id or not isinstance(persona_id, str) or not persona_id:
continue
key = (group_id, persona_id)
items.append({
"group_id": group_id,
"group_title": g.get("title"),
"persona_id": persona_id,
"persona_name": p.get("name"),
"tier": p.get("tier"),
"my_outcome": outcome_by.get(key, "not_tried"),
})
return jsonify({"board": items})
@me_bp.get("/weak-areas")
@require_auth
def weak_areas():
s = _stores()
actor = current_user()
uid = actor["id"]
org_id = actor.get("org_id")
if not is_valid_tenant_id(org_id):
raise ApiError("permission denied", 403)
sessions = _authorized_finished_trainee_sessions(s, actor)
insight = analyze_weak_areas(sessions, user_id=uid, org_id=org_id)
return jsonify({"insight": insight})
def _personal_group(s, actor) -> dict:
"""Return (or create) the user's private group holding their own personas."""
org_id = actor.get("org_id")
if not is_valid_tenant_id(org_id):
raise ApiError("permission denied", 403)
try:
return s["groups"].get_or_create_private_group(
org_id=org_id,
owner_user_id=actor["id"],
owner_name=actor.get("name", "User"),
)
except ValueError as exc:
raise internal_error("private group unavailable", exc, 409)
@me_bp.get("/personas")
@require_auth
def my_personas():
s = _stores()
actor = current_user()
group = _personal_group(s, actor)
from .group_routes import serialize_group, serialize_persona
raw_personas = group.get("personas")
personas = raw_personas if isinstance(raw_personas, list) else []
return jsonify({
"group": serialize_group(group, actor),
"personas": [serialize_persona(p, actor) for p in personas if isinstance(p, dict)],
})
@me_bp.post("/personas/generate")
@require_auth
def generate_persona():
s = _stores()
actor = current_user()
data = request_json_object()
mode = data.get("mode", "manual") # "weak-area" | "manual"
spec = data.get("spec") or {}
if not isinstance(mode, str) or not isinstance(spec, dict):
raise ApiError("invalid request", 400)
llm = s["llm"]
if not llm:
raise ApiError("LLM not configured", 500)
if mode == "weak-area" and not spec:
# auto-detect weak areas from this user's losses if no spec given
org_id = actor.get("org_id")
if not is_valid_tenant_id(org_id):
raise ApiError("permission denied", 403)
sessions = _authorized_finished_trainee_sessions(s, actor)
spec = analyze_weak_areas(sessions, user_id=actor["id"], org_id=org_id)
from ..services.own_persona import generate_own_persona
try:
persona = generate_own_persona(llm, mode=mode, spec=spec)
except (LLMError, ValueError) as exc:
raise internal_error("persona generation failed", exc)
group = _personal_group(s, actor)
with s["groups"].record_lock(group["id"]):
group = s["groups"].get(group["id"])
raw_personas = group.get("personas")
existing = [p for p in raw_personas if isinstance(p, dict)] if isinstance(raw_personas, list) else []
persona["id"] = f"myp-{uuid.uuid4().hex[:10]}"
existing.append(persona)
s["groups"].set_personas(group["id"], existing)
from .group_routes import serialize_group, serialize_persona
fresh_group = s["groups"].get(group["id"])
return jsonify({
"persona": serialize_persona(persona, actor),
"group": serialize_group(fresh_group, actor),
}), 201