Implement OpenRouter Model Support in AI Prompt System
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added `openrouter_model` field to the `ai_prompts` table, allowing for optional model overrides per prompt. - Updated the `exercise_ai` module to utilize the effective OpenRouter model based on prompt-specific settings, enhancing flexibility in AI interactions. - Enhanced the admin interface to support OpenRouter model configuration for prompts, improving usability for Superadmins. - Incremented application version to 0.8.161 and updated changelog to reflect these changes, including migration details and new functionality.
This commit is contained in:
parent
0551bb3d80
commit
93b8d09d05
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
**Ist-Stand API (Superadmin):**
|
**Ist-Stand API (Superadmin):**
|
||||||
- `GET /api/admin/ai-prompts`, `GET /api/admin/ai-prompts/{id}`, `PUT …`, `POST …/preview`, `POST …/reset-template`, `GET /api/admin/ai-prompts/catalog/placeholders`
|
- `GET /api/admin/ai-prompts`, `GET /api/admin/ai-prompts/{id}`, `PUT …`, `POST …/preview`, `POST …/reset-template`, `GET /api/admin/ai-prompts/catalog/placeholders`
|
||||||
|
- Spalte **`openrouter_model`** (Migration **070**): Optional pro Prompt-Zeile; OpenRouter **`model`**-Parameter; **`NULL`/leer ⇒ `OPENROUTER_MODEL`** aus der Umgebung.
|
||||||
|
|
||||||
**Autor:** Claude Code
|
**Autor:** Claude Code
|
||||||
**Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System
|
**Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ flowchart LR
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
| **P0** | `AiPromptContextKind`, `load_ai_prompt_row` zentral; Übungs-KI über Laufzeit. |
|
| **P0** | `AiPromptContextKind`, `load_ai_prompt_row` zentral; Übungs-KI über Laufzeit. |
|
||||||
| **P1 (teilweise)** | `load_and_render_ai_prompt`, `AiPromptUnavailableError`, `render_ai_prompt_template_for_row`; **Pydantic** `ExerciseFormAiPromptContext` / `resolve_exercise_form_variables` in `ai_prompt_job` (Admin-Vorschau + gemeinsame Schnittstelle). Nächster Schritt: `POST /exercises/ai/suggest` kann dasselbe Kontextmodell nutzen; optionale `execute_*`-Fassade fuer OpenRouter-Schritt. |
|
| **P1 (teilweise)** | `load_and_render_ai_prompt`, `AiPromptUnavailableError`, `render_ai_prompt_template_for_row`; **Pydantic** `ExerciseFormAiPromptContext` / `resolve_exercise_form_variables` in `ai_prompt_job` (Admin-Vorschau + gemeinsame Schnittstelle). Nächster Schritt: `POST /exercises/ai/suggest` kann dasselbe Kontextmodell nutzen; optionale `execute_*`-Fassade fuer OpenRouter-Schritt. |
|
||||||
| **P2** | Versionierung oder Audit-Spalten; optionale Modell-/Temperatur-Overrides pro Slug in DB oder Config-Tabelle. |
|
| **P2** | Versionierung oder Audit-Spalten; **teilweise:** optionales OpenRouter-Modell pro Zeile (`openrouter_model`, Migration 070, Fallback `OPENROUTER_MODEL`); weitere Overrides (Temperatur) offen. |
|
||||||
| **P3** | Composition/Segmente (JSON Schema Version 1) + UI nur für komplexe Slugs. |
|
| **P3** | Composition/Segmente (JSON Schema Version 1) + UI nur für komplexe Slugs. |
|
||||||
| **P4** | Erste Planungs-/Rahmen-Slugs mit dedizierten Buildern und Token-Budget-Strategien. |
|
| **P4** | Erste Planungs-/Rahmen-Slugs mit dedizierten Buildern und Token-Budget-Strategien. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ DB_PASSWORD=CHANGE_ME_SECURE_PASSWORD
|
||||||
|
|
||||||
OPENROUTER_API_KEY=your_api_key_here
|
OPENROUTER_API_KEY=your_api_key_here
|
||||||
OPENROUTER_MODEL=anthropic/claude-sonnet-4
|
OPENROUTER_MODEL=anthropic/claude-sonnet-4
|
||||||
|
# Standard-OpenRouter-Modell (alle Aufrufe). Optional pro Prompt in ai_prompts.openrouter_model
|
||||||
|
# ueberschreibbar (Migration 070, Superadmin unter „KI Prompts“).
|
||||||
|
|
||||||
# Übungs-KI (Docker): ohne Eintrag im compose „environment:“ landet keine .env-Zeile im Container.
|
# Übungs-KI (Docker): ohne Eintrag im compose „environment:“ landet keine .env-Zeile im Container.
|
||||||
# Hier ist SHINKAN_AI_DEBUG in docker-compose*.yml angebunden — 1 = ausführliche WARN-Logs (exercise_ai, openrouter).
|
# Hier ist SHINKAN_AI_DEBUG in docker-compose*.yml angebunden — 1 = ausführliche WARN-Logs (exercise_ai, openrouter).
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ class ExerciseFormAiPromptContext(BaseModel):
|
||||||
"""
|
"""
|
||||||
Eingabe fuer Uebungsbezogene Prompts (Kurzfassung / Skill-JSON-Vorschlag).
|
Eingabe fuer Uebungsbezogene Prompts (Kurzfassung / Skill-JSON-Vorschlag).
|
||||||
Entspricht fachlich dem Preview-Body unter /api/admin/ai-prompts/*/preview.
|
Entspricht fachlich dem Preview-Body unter /api/admin/ai-prompts/*/preview.
|
||||||
|
|
||||||
|
Abgrenzung: POST /exercises/ai/suggest nutzt ExerciseAiSuggestBody mit include_summary /
|
||||||
|
include_skills usw.; dieses Modell bildet nur die gemeinsamen Formularfelder — wie die
|
||||||
|
Admin-Vorschau-Body (AiPromptPreviewBody).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title: Optional[str] = ""
|
title: Optional[str] = ""
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ def load_ai_prompt_row(cur, slug: str, *, active_only: bool = True) -> Optional[
|
||||||
if active_only:
|
if active_only:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT slug, display_name, template, output_format, active
|
SELECT slug, display_name, template, output_format, active, openrouter_model
|
||||||
FROM ai_prompts
|
FROM ai_prompts
|
||||||
WHERE slug = %s AND active = true
|
WHERE slug = %s AND active = true
|
||||||
""",
|
""",
|
||||||
|
|
@ -54,7 +54,7 @@ def load_ai_prompt_row(cur, slug: str, *, active_only: bool = True) -> Optional[
|
||||||
else:
|
else:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT slug, display_name, template, output_format, active
|
SELECT slug, display_name, template, output_format, active, openrouter_model
|
||||||
FROM ai_prompts
|
FROM ai_prompts
|
||||||
WHERE slug = %s
|
WHERE slug = %s
|
||||||
""",
|
""",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,13 @@ from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Sequence,
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from openrouter_chat import OpenRouterError, normalize_openrouter_env, openrouter_chat_completion
|
from openrouter_chat import (
|
||||||
|
OpenRouterError,
|
||||||
|
default_openrouter_model_id,
|
||||||
|
effective_openrouter_model_for_prompt_row,
|
||||||
|
normalize_openrouter_env,
|
||||||
|
openrouter_chat_completion,
|
||||||
|
)
|
||||||
|
|
||||||
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
|
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
|
||||||
|
|
||||||
|
|
@ -663,14 +669,14 @@ def _sanitize_skill_entries(cur, rows: Any) -> List[Dict[str, Any]]:
|
||||||
return out[:5]
|
return out[:5]
|
||||||
|
|
||||||
|
|
||||||
def _require_openrouter() -> Tuple[str, str]:
|
def _require_openrouter_key() -> str:
|
||||||
key, model = normalize_openrouter_env()
|
key, _ = normalize_openrouter_env()
|
||||||
if not key:
|
if not key:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="KI nicht konfiguriert (OPENROUTER_API_KEY fehlt).",
|
detail="KI nicht konfiguriert (OPENROUTER_API_KEY fehlt).",
|
||||||
)
|
)
|
||||||
return key, model
|
return key
|
||||||
|
|
||||||
|
|
||||||
def run_exercise_ai_suggestion(
|
def run_exercise_ai_suggestion(
|
||||||
|
|
@ -684,7 +690,7 @@ def run_exercise_ai_suggestion(
|
||||||
want_summary: bool,
|
want_summary: bool,
|
||||||
want_skills: bool,
|
want_skills: bool,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
key, model = _require_openrouter()
|
key = _require_openrouter_key()
|
||||||
|
|
||||||
g_plain = strip_html_to_plain(goal)
|
g_plain = strip_html_to_plain(goal)
|
||||||
e_plain = strip_html_to_plain(execution)
|
e_plain = strip_html_to_plain(execution)
|
||||||
|
|
@ -697,7 +703,8 @@ def run_exercise_ai_suggestion(
|
||||||
t_title = (title or "").strip()
|
t_title = (title or "").strip()
|
||||||
focus = (focus_area_hint or "").strip()
|
focus = (focus_area_hint or "").strip()
|
||||||
|
|
||||||
result: Dict[str, Any] = {"model": model}
|
result: Dict[str, Any] = {}
|
||||||
|
models_by_slug: Dict[str, str] = {}
|
||||||
|
|
||||||
if _ai_debug_on():
|
if _ai_debug_on():
|
||||||
fid_list = ",".join(str(x) for x in _ordered_focus_ids(focus_areas_context))
|
fid_list = ",".join(str(x) for x in _ordered_focus_ids(focus_areas_context))
|
||||||
|
|
@ -733,6 +740,8 @@ def run_exercise_ai_suggestion(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Prompt exercise_summary nicht aktiv oder fehlt in DB.",
|
detail="Prompt exercise_summary nicht aktiv oder fehlt in DB.",
|
||||||
) from None
|
) from None
|
||||||
|
model_summary = effective_openrouter_model_for_prompt_row(prow)
|
||||||
|
models_by_slug["exercise_summary"] = model_summary
|
||||||
prompt = rendered.text
|
prompt = rendered.text
|
||||||
if _ai_debug_on():
|
if _ai_debug_on():
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
|
@ -741,7 +750,7 @@ def run_exercise_ai_suggestion(
|
||||||
len(rendered.placeholders_remaining),
|
len(rendered.placeholders_remaining),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
raw = openrouter_chat_completion(api_key=key, model=model, user_content=prompt)
|
raw = openrouter_chat_completion(api_key=key, model=model_summary, user_content=prompt)
|
||||||
except OpenRouterError as e:
|
except OpenRouterError as e:
|
||||||
raise HTTPException(status_code=502, detail=f"OpenRouter: {e}") from e
|
raise HTTPException(status_code=502, detail=f"OpenRouter: {e}") from e
|
||||||
if _ai_debug_on():
|
if _ai_debug_on():
|
||||||
|
|
@ -754,7 +763,7 @@ def run_exercise_ai_suggestion(
|
||||||
)
|
)
|
||||||
if len(text) > _MAX_SUMMARY_CHARS:
|
if len(text) > _MAX_SUMMARY_CHARS:
|
||||||
text = text[: _MAX_SUMMARY_CHARS - 1].rstrip() + "…"
|
text = text[: _MAX_SUMMARY_CHARS - 1].rstrip() + "…"
|
||||||
result["summary"] = {"text": text, "ai_generated": True, "model": model}
|
result["summary"] = {"text": text, "ai_generated": True, "model": model_summary}
|
||||||
|
|
||||||
if want_skills:
|
if want_skills:
|
||||||
try:
|
try:
|
||||||
|
|
@ -776,6 +785,8 @@ def run_exercise_ai_suggestion(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Prompt exercise_skill_suggestions nicht aktiv oder fehlt in DB.",
|
detail="Prompt exercise_skill_suggestions nicht aktiv oder fehlt in DB.",
|
||||||
) from None
|
) from None
|
||||||
|
model_skills = effective_openrouter_model_for_prompt_row(srow)
|
||||||
|
models_by_slug["exercise_skill_suggestions"] = model_skills
|
||||||
prompt = rendered.text
|
prompt = rendered.text
|
||||||
if _ai_debug_on():
|
if _ai_debug_on():
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
|
@ -791,7 +802,7 @@ def run_exercise_ai_suggestion(
|
||||||
try:
|
try:
|
||||||
raw = openrouter_chat_completion(
|
raw = openrouter_chat_completion(
|
||||||
api_key=key,
|
api_key=key,
|
||||||
model=model,
|
model=model_skills,
|
||||||
user_content=prompt,
|
user_content=prompt,
|
||||||
system_content=sys_hint,
|
system_content=sys_hint,
|
||||||
temperature=0.15,
|
temperature=0.15,
|
||||||
|
|
@ -829,6 +840,14 @@ def run_exercise_ai_suggestion(
|
||||||
|
|
||||||
result["skills"] = skills
|
result["skills"] = skills
|
||||||
|
|
||||||
|
result["models_by_slug"] = models_by_slug
|
||||||
|
if want_skills:
|
||||||
|
result["model"] = models_by_slug["exercise_skill_suggestions"]
|
||||||
|
elif want_summary:
|
||||||
|
result["model"] = models_by_slug["exercise_summary"]
|
||||||
|
else:
|
||||||
|
result["model"] = default_openrouter_model_id()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
7
backend/migrations/070_ai_prompts_openrouter_model.sql
Normal file
7
backend/migrations/070_ai_prompts_openrouter_model.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Migration 070: optionales OpenRouter-Modell pro Prompt-Zeile
|
||||||
|
-- Leer/NULL → Umgebungsvariable OPENROUTER_MODEL (wie bisher).
|
||||||
|
|
||||||
|
ALTER TABLE ai_prompts ADD COLUMN IF NOT EXISTS openrouter_model VARCHAR(200);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN ai_prompts.openrouter_model IS
|
||||||
|
'Optional: OpenRouter model id (z.B. anthropic/claude-3.5-haiku); NULL = OPENROUTER_MODEL aus Env';
|
||||||
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Mapping, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|
@ -203,3 +203,22 @@ def normalize_openrouter_env() -> tuple[str, str]:
|
||||||
key = (os.getenv("OPENROUTER_API_KEY") or "").strip()
|
key = (os.getenv("OPENROUTER_API_KEY") or "").strip()
|
||||||
model = (os.getenv("OPENROUTER_MODEL") or "anthropic/claude-sonnet-4").strip()
|
model = (os.getenv("OPENROUTER_MODEL") or "anthropic/claude-sonnet-4").strip()
|
||||||
return key, model
|
return key, model
|
||||||
|
|
||||||
|
|
||||||
|
def default_openrouter_model_id() -> str:
|
||||||
|
"""Standard-Modell aus OPENROUTER_MODEL (ohne API-Key zu pruefen)."""
|
||||||
|
_, model = normalize_openrouter_env()
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def effective_openrouter_model_for_prompt_row(row: Optional[Mapping[str, Any]]) -> str:
|
||||||
|
"""
|
||||||
|
Pro-Prompt-Override in ai_prompts.openrouter_model, sonst Env-Default.
|
||||||
|
|
||||||
|
`row` kann eine partial Row aus load_ai_prompt_row sein (Felder slug, openrouter_model, …).
|
||||||
|
"""
|
||||||
|
if row:
|
||||||
|
custom = str(row.get("openrouter_model") or "").strip()
|
||||||
|
if custom:
|
||||||
|
return custom
|
||||||
|
return default_openrouter_model_id()
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ def _fetch_prompt_any(cur, prompt_id: int) -> Dict[str, Any]:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, slug, display_name, description, template, category, output_format,
|
SELECT id, slug, display_name, description, template, category, output_format,
|
||||||
output_schema, is_system_default, default_template,
|
output_schema, is_system_default, default_template, openrouter_model,
|
||||||
active, sort_order, created_at, updated_at
|
active, sort_order, created_at, updated_at
|
||||||
FROM ai_prompts WHERE id = %s
|
FROM ai_prompts WHERE id = %s
|
||||||
""",
|
""",
|
||||||
|
|
@ -57,6 +57,7 @@ class AiPromptUpdateBody(BaseModel):
|
||||||
active: Optional[bool] = None
|
active: Optional[bool] = None
|
||||||
display_name: Optional[str] = Field(None, max_length=200)
|
display_name: Optional[str] = Field(None, max_length=200)
|
||||||
description: Optional[str] = Field(None, max_length=8000)
|
description: Optional[str] = Field(None, max_length=8000)
|
||||||
|
openrouter_model: Optional[str] = Field(None, max_length=200)
|
||||||
|
|
||||||
|
|
||||||
class AiPromptPreviewFocus(BaseModel):
|
class AiPromptPreviewFocus(BaseModel):
|
||||||
|
|
@ -86,7 +87,7 @@ def list_ai_prompts(session: dict = Depends(_require_superadmin)):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, slug, display_name, description, category, output_format, active,
|
SELECT id, slug, display_name, description, category, output_format, active,
|
||||||
sort_order, is_system_default, default_template
|
sort_order, is_system_default, default_template, openrouter_model
|
||||||
FROM ai_prompts
|
FROM ai_prompts
|
||||||
ORDER BY sort_order ASC NULLS LAST, id ASC
|
ORDER BY sort_order ASC NULLS LAST, id ASC
|
||||||
"""
|
"""
|
||||||
|
|
@ -150,16 +151,25 @@ def update_ai_prompt(
|
||||||
next_desc = body.description if body.description is not None else old.get("description") or ""
|
next_desc = body.description if body.description is not None else old.get("description") or ""
|
||||||
next_desc = (next_desc or "").strip()
|
next_desc = (next_desc or "").strip()
|
||||||
|
|
||||||
|
next_openrouter = old.get("openrouter_model")
|
||||||
|
if body.openrouter_model is not None:
|
||||||
|
cand = body.openrouter_model.strip() if isinstance(body.openrouter_model, str) else ""
|
||||||
|
if any(c in cand for c in ("\r", "\n", "\t")):
|
||||||
|
raise HTTPException(status_code=400, detail="openrouter_model: keine Steuerzeichen erlaubt.")
|
||||||
|
next_openrouter = cand or None
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE ai_prompts
|
UPDATE ai_prompts
|
||||||
SET template = %s, active = %s, display_name = %s, description = %s, updated_at = NOW()
|
SET template = %s, active = %s, display_name = %s, description = %s,
|
||||||
|
openrouter_model = %s, updated_at = NOW()
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
RETURNING id, slug, display_name, description, template, category, output_format,
|
RETURNING id, slug, display_name, description, template, category, output_format,
|
||||||
output_schema, is_system_default, default_template, active, sort_order,
|
output_schema, is_system_default, default_template, openrouter_model,
|
||||||
|
active, sort_order,
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
""",
|
""",
|
||||||
(next_template, next_active, next_name, next_desc, prompt_id),
|
(next_template, next_active, next_name, next_desc, next_openrouter, prompt_id),
|
||||||
)
|
)
|
||||||
row = dict(cur.fetchone())
|
row = dict(cur.fetchone())
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
@ -188,7 +198,8 @@ def reset_ai_prompt_template(prompt_id: int, session: dict = Depends(_require_su
|
||||||
SET template = default_template, updated_at = NOW()
|
SET template = default_template, updated_at = NOW()
|
||||||
WHERE id = %s AND default_template IS NOT NULL
|
WHERE id = %s AND default_template IS NOT NULL
|
||||||
RETURNING id, slug, display_name, description, template, category, output_format,
|
RETURNING id, slug, display_name, description, template, category, output_format,
|
||||||
output_schema, is_system_default, default_template, active, sort_order,
|
output_schema, is_system_default, default_template, openrouter_model,
|
||||||
|
active, sort_order,
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
""",
|
""",
|
||||||
(prompt_id,),
|
(prompt_id,),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.160"
|
APP_VERSION = "0.8.161"
|
||||||
BUILD_DATE = "2026-05-30"
|
BUILD_DATE = "2026-05-31"
|
||||||
DB_SCHEMA_VERSION = "20260530069"
|
DB_SCHEMA_VERSION = "20260531070"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
||||||
|
|
@ -19,14 +19,14 @@ MODULE_VERSIONS = {
|
||||||
"media_legal_hold": "1.0.0", # P-11: Sofortsperre-Services (set_legal_hold, release_legal_hold)
|
"media_legal_hold": "1.0.0", # P-11: Sofortsperre-Services (set_legal_hold, release_legal_hold)
|
||||||
"media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets
|
"media_lifecycle": "1.1.0", # P-11: Retention-Job ueberspringt Legal-Hold-Assets
|
||||||
"admin_ai_skill_retrieval": "1.0.0", # Superadmin CRUD /api/admin/ai-skill-retrieval-profiles (Migration 068)
|
"admin_ai_skill_retrieval": "1.0.0", # Superadmin CRUD /api/admin/ai-skill-retrieval-profiles (Migration 068)
|
||||||
"admin_ai_prompts": "1.0.2", # Preview: ai_prompt_job + render_ai_prompt_template_for_row
|
"admin_ai_prompts": "1.0.3", # Migration 070: openrouter_model; PUT/Liste/Detail
|
||||||
"ai_prompt_job": "0.1.0", # ExerciseFormAiPromptContext, resolve_exercise_form_variables — P1 Kontext-Schnittstelle
|
"ai_prompt_job": "0.1.0", # ExerciseFormAiPromptContext, resolve_exercise_form_variables — P1 Kontext-Schnittstelle
|
||||||
"ai_prompt_runtime": "0.2.0", # load_and_render_ai_prompt, AiPromptUnavailableError, render_ai_prompt_template_for_row
|
"ai_prompt_runtime": "0.2.0", # load_and_render_ai_prompt, AiPromptUnavailableError, render_ai_prompt_template_for_row
|
||||||
"groups": "0.1.0",
|
"groups": "0.1.0",
|
||||||
"skills": "0.1.1", # DB 065 karate_relevance + relevance_level; CRUD unterstützt Felder
|
"skills": "0.1.1", # DB 065 karate_relevance + relevance_level; CRUD unterstützt Felder
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.31.2", # exercise_ai: load_and_render_ai_prompt statt getrennt load+render
|
"exercises": "2.31.3", # exercise_ai: OpenRouter-Modell pro Prompt-Slug; Response models_by_slug
|
||||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||||
|
|
@ -41,6 +41,15 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.161",
|
||||||
|
"date": "2026-05-31",
|
||||||
|
"changes": [
|
||||||
|
"Migration 070: ai_prompts.openrouter_model (optional je Prompt; Fallback OPENROUTER_MODEL).",
|
||||||
|
"exercise_ai: effektives OpenRouter-Modell pro Slug; API-Response models_by_slug + model (Skills bevorzugt).",
|
||||||
|
"Superadmin „KI Prompts“: OpenRouter-Modell speicherbar.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.160",
|
"version": "0.8.160",
|
||||||
"date": "2026-05-30",
|
"date": "2026-05-30",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export default function AdminAiPromptsPage() {
|
||||||
const [draftName, setDraftName] = useState('')
|
const [draftName, setDraftName] = useState('')
|
||||||
const [draftDesc, setDraftDesc] = useState('')
|
const [draftDesc, setDraftDesc] = useState('')
|
||||||
const [draftTemplate, setDraftTemplate] = useState('')
|
const [draftTemplate, setDraftTemplate] = useState('')
|
||||||
|
const [draftOpenrouterModel, setDraftOpenrouterModel] = useState('')
|
||||||
const [draftActive, setDraftActive] = useState(true)
|
const [draftActive, setDraftActive] = useState(true)
|
||||||
|
|
||||||
const [pvTitle, setPvTitle] = useState('Testübung')
|
const [pvTitle, setPvTitle] = useState('Testübung')
|
||||||
|
|
@ -74,6 +75,9 @@ export default function AdminAiPromptsPage() {
|
||||||
setDraftName(d.display_name || '')
|
setDraftName(d.display_name || '')
|
||||||
setDraftDesc(d.description || '')
|
setDraftDesc(d.description || '')
|
||||||
setDraftTemplate(d.template || '')
|
setDraftTemplate(d.template || '')
|
||||||
|
setDraftOpenrouterModel(
|
||||||
|
typeof d.openrouter_model === 'string' ? d.openrouter_model : ''
|
||||||
|
)
|
||||||
setDraftActive(!!d.active)
|
setDraftActive(!!d.active)
|
||||||
setPvPreview(null)
|
setPvPreview(null)
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +100,7 @@ export default function AdminAiPromptsPage() {
|
||||||
display_name: draftName,
|
display_name: draftName,
|
||||||
description: draftDesc,
|
description: draftDesc,
|
||||||
active: draftActive,
|
active: draftActive,
|
||||||
|
openrouter_model: draftOpenrouterModel.trim(),
|
||||||
})
|
})
|
||||||
await loadList()
|
await loadList()
|
||||||
const nd = await api.getAdminAiPrompt(detail.id)
|
const nd = await api.getAdminAiPrompt(detail.id)
|
||||||
|
|
@ -201,6 +206,11 @@ export default function AdminAiPromptsPage() {
|
||||||
inaktiv
|
inaktiv
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{p.openrouter_model ? (
|
||||||
|
<span style={{ fontSize: 11, color: 'var(--text3)' }} title="OpenRouter-Modell für diesen Prompt">
|
||||||
|
Model: <code>{p.openrouter_model}</code>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{p.is_modified ? <span style={{ fontSize: 11 }}>(von Referenz abweichend)</span> : null}
|
{p.is_modified ? <span style={{ fontSize: 11 }}>(von Referenz abweichend)</span> : null}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -231,6 +241,17 @@ export default function AdminAiPromptsPage() {
|
||||||
onChange={(e) => setDraftDesc(e.target.value)}
|
onChange={(e) => setDraftDesc(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">OpenRouter-Modell (optional)</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Leer = Server-OPENROUTER_MODEL · z.B. anthropic/claude-3.5-haiku"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
value={draftOpenrouterModel}
|
||||||
|
onChange={(e) => setDraftOpenrouterModel(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
<input type="checkbox" checked={draftActive} onChange={(e) => setDraftActive(e.target.checked)} />
|
<input type="checkbox" checked={draftActive} onChange={(e) => setDraftActive(e.target.checked)} />
|
||||||
Aktiv
|
Aktiv
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user