Files
Crumb-Core-v.1/app/routers/crumbforest_roles.py

338 lines
11 KiB
Python

from fastapi import APIRouter, Depends, Request, Form, HTTPException, Body
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
import httpx
from typing import Optional, List, Dict, Any
from pydantic import BaseModel
from deps import current_user
from services.config_loader import ConfigLoader
from services.localization import merge_role_localization
from config import get_settings
router = APIRouter()
settings = get_settings()
@router.get("/crumbforest/roles", response_class=HTMLResponse)
async def roles_dashboard(req: Request, user = Depends(current_user)):
"""
Show available roles based on user's group.
"""
# Determine user group
if not user:
user_group = 'home'
else:
user_group = user.get('user_group', 'demo')
# Get Language
lang = req.query_params.get("lang") or req.session.get("lang") or "de"
req.session["lang"] = lang
# Load config
config = ConfigLoader.load_config()
all_roles_raw = config.get('roles', {})
# Localize roles
all_roles = merge_role_localization(all_roles_raw, lang)
# Filter roles by group access
available_roles = {
rid: role for rid, role in all_roles.items()
if user_group in role.get('group_access', [])
}
# Get group config for template rendering
group_config = config.get('groups', {}).get(user_group, {})
return req.app.state.render(
req,
"crumbforest/roles_dashboard.html",
roles=available_roles,
user_group=user_group,
group_config=group_config,
lang=lang
)
@router.get("/crumbforest/roles/{role_id}", response_class=HTMLResponse)
async def role_chat(req: Request, role_id: str, user = Depends(current_user)):
"""
Chat interface for a specific role.
"""
# Get Language
lang = req.query_params.get("lang") or req.session.get("lang") or "de"
req.session["lang"] = lang
config = ConfigLoader.load_config()
# Localize single role lookup
# Only need to localize the specific role, but easiest to localize all then pick
all_roles = merge_role_localization(config.get('roles', {}), lang)
role = all_roles.get(role_id)
if not role:
raise HTTPException(404, "Role not found")
# Determine user group
if not user:
user_group = 'home'
else:
user_group = user.get('user_group', 'demo')
# Check access
if user_group not in role.get('group_access', []):
# Redirect to login if not logged in, else 403
if not user:
# Fix: Use /{lang}/login instead of /login to prevent matching {lang}=login
return RedirectResponse(f"/{lang}/login?next={req.url.path}", status_code=302)
raise HTTPException(403, "Access denied")
group_config = config.get('groups', {}).get(user_group, {})
return req.app.state.render(
req,
"crumbforest/role_chat.html",
role=role,
group_config=group_config,
lang=lang
)
@router.post("/crumbforest/roles/{role_id}/ask")
async def ask_role(
req: Request,
role_id: str,
question: str = Form(...),
user = Depends(current_user)
):
"""
Send question to role and get AI response.
"""
config = ConfigLoader.load_config()
role = config.get('roles', {}).get(role_id)
if not role:
raise HTTPException(404, "Role not found")
# Access check (simplified for API)
if not user:
raise HTTPException(401, "Login required")
user_group = user.get('user_group', 'demo')
if user_group not in role.get('group_access', []):
raise HTTPException(403, "Access denied")
# Get Language from session (persisted by dashboard/chat page view)
lang = req.session.get("lang", "de")
# Merge localization for this role again to be sure (in case session changed or not applied)
# Actually, config loaded above might not be localized if we used ConfigLoader directly.
# Let's import merge_role_localization
from services.localization import merge_role_localization
localized_roles = merge_role_localization(config.get('roles', {}), lang)
localized_role = localized_roles.get(role_id)
if localized_role:
role = localized_role
# Initialize RAG Service
from services.provider_factory import ProviderFactory
from utils.rag_chat import RAGChatService
from deps import get_qdrant_client
if not settings.openrouter_api_key:
return JSONResponse({
"answer": "⚠️ OpenRouter API Key missing.",
"error": "config_error"
}, status_code=500)
try:
embedding_provider = ProviderFactory.create_provider("openrouter", settings)
completion_provider = ProviderFactory.create_provider("openrouter", settings)
qdrant_client = get_qdrant_client()
rag_service = RAGChatService(
qdrant_client=qdrant_client,
embedding_provider=embedding_provider,
completion_provider=completion_provider
)
# Generate answer with RAG
# We pass role['system_prompt'] which is now localized!
result = rag_service.chat_with_context(
question=question,
character_name=role['name'],
character_prompt=role['system_prompt'],
context_limit=3,
lang=lang,
model_override=role.get('model') # Support specific models per role
)
answer = result['answer']
# Update history (Session based for now)
history_key = f'role_history_{role_id}'
history = req.session.get(history_key, [])
history.append({"role": "user", "content": question})
history.append({"role": "assistant", "content": answer})
req.session[history_key] = history[-10:]
# Log interaction (for Admin Logs)
try:
from utils.chat_logger import ChatLogger
logger = ChatLogger()
session_id = req.session.get("session_id", "anonymous")
logger.log_interaction(
character_id=role_id,
character_name=role.get('name', role_id),
user_id=user.get("id") if user else "anonymous",
user_role=user.get("role") if user else "anonymous",
question=question,
answer=answer,
model=result.get("model", "unknown"),
provider=result.get("provider", "unknown"),
context_found=result.get("context_found", False),
sources_count=len(result.get("sources", [])),
lang=lang,
session_id=session_id
)
except Exception as log_err:
print(f"⚠️ Logger failed: {log_err}")
return JSONResponse({
"role": role_id,
"question": question,
"answer": answer,
"usage": {}, # RAG service might not return usage exactly same way, or we skip
"sources": result.get("sources", []) # Return sources!
})
except Exception as e:
print(f"Exception in ask_role: {e}")
return JSONResponse({
"answer": "⚠️ Internal server error (RAG).",
"details": str(e)
}, status_code=500)
# --- Settings Endpoints ---
@router.get("/settings", response_class=HTMLResponse)
async def settings_page(req: Request, user = Depends(current_user)):
"""
User settings page.
"""
if not user:
return RedirectResponse(f"/login?next={req.url.path}", status_code=302)
config = ConfigLoader.load_config()
user_group = user.get('user_group', 'demo')
group_config = config.get('groups', {}).get(user_group, {})
theme_variants = config.get('theme_variants', {})
return req.app.state.render(
req,
"pages/settings.html",
group_config=group_config,
theme_variants=theme_variants
)
@router.post("/settings/theme")
async def update_theme(
req: Request,
theme: str = Form(...),
user = Depends(current_user)
):
"""
Update user theme preference.
"""
if not user:
raise HTTPException(401, "Login required")
# Verify theme exists
config = ConfigLoader.load_config()
if theme not in config.get('theme_variants', {}):
raise HTTPException(400, "Invalid theme")
# Update session
user['theme'] = theme
req.session['user'] = user
# Update DB
try:
from ..deps import get_db
from pymysql.cursors import DictCursor
with get_db().cursor(DictCursor) as cur:
cur.execute("UPDATE users SET theme=%s WHERE id=%s", (theme, user['id']))
except Exception as e:
print(f"Error updating theme in DB: {e}")
# Continue anyway, session is updated
return RedirectResponse("/settings", status_code=302)
@router.post("/settings/accessibility")
async def update_accessibility(
req: Request,
high_contrast: Optional[bool] = Form(None),
animation_reduced: Optional[bool] = Form(None),
font_size: str = Form("normal"),
user = Depends(current_user)
):
"""
Update accessibility settings.
"""
if not user:
raise HTTPException(401, "Login required")
import json
accessibility = {
"high_contrast": bool(high_contrast),
"animation_reduced": bool(animation_reduced),
"font_size": font_size
}
# Update session
user['accessibility'] = accessibility
# If high contrast is enabled, force that theme, otherwise revert to group default or keep current?
# For now, we just set the accessibility flags. The theme switcher handles the main theme.
# However, if high_contrast is ON, we might want to auto-switch to pico-high-contrast.
if accessibility['high_contrast']:
user['theme'] = 'pico-high-contrast'
elif user['theme'] == 'pico-high-contrast' and not accessibility['high_contrast']:
# Revert to default if turning off high contrast
user['theme'] = 'pico-default'
req.session['user'] = user
# Update DB
try:
from deps import get_db
from pymysql.cursors import DictCursor
with get_db().cursor(DictCursor) as cur:
cur.execute(
"UPDATE users SET accessibility=%s, theme=%s WHERE id=%s",
(json.dumps(accessibility), user['theme'], user['id'])
)
except Exception as e:
print(f"Error updating accessibility in DB: {e}")
return RedirectResponse("/settings", status_code=302)
@router.get("/crumbforest/roles/{role_id}/export")
async def export_history(req: Request, role_id: str, user = Depends(current_user)):
"""
Export chat history as JSON.
"""
if not user:
raise HTTPException(401, "Login required")
history_key = f'role_history_{role_id}'
history = req.session.get(history_key, [])
return JSONResponse({
"role": role_id,
"exported_at": str(import_datetime().now()),
"history": history
})
def import_datetime():
from datetime import datetime
return datetime