Implement ExerciseFormAiPromptContext and Refactor AI Prompt Job Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 40s
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 34s
Test Suite / playwright-tests (push) Successful in 1m23s
All checks were successful
Deploy Development / deploy (push) Successful in 40s
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 34s
Test Suite / playwright-tests (push) Successful in 1m23s
- Introduced `ExerciseFormAiPromptContext` for unified handling of prompt-related data, enhancing the admin preview and exercise API. - Added `run_exercise_form_ai_suggestion` function to streamline AI suggestion processing, integrating with the OpenRouter. - Updated various modules to utilize the new context model, improving code clarity and reducing redundancy. - Incremented application version to 0.8.162 and updated changelog to reflect these changes, including migration details and new functionality.
This commit is contained in:
parent
93b8d09d05
commit
5331eab39c
|
|
@ -139,7 +139,7 @@ flowchart LR
|
||||||
| Phase | Inhalt |
|
| Phase | Inhalt |
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
| **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** | `load_and_render_ai_prompt`, `AiPromptUnavailableError`, `render_ai_prompt_template_for_row`; **`ExerciseFormAiPromptContext`** in `ai_prompt_context.py`; **`run_exercise_form_ai_suggestion`**; Übungs-API und Admin-Vorschau nutzen denselben Kontext. |
|
||||||
| **P2** | Versionierung oder Audit-Spalten; **teilweise:** optionales OpenRouter-Modell pro Zeile (`openrouter_model`, Migration 070, Fallback `OPENROUTER_MODEL`); weitere Overrides (Temperatur) offen. |
|
| **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. |
|
||||||
|
|
@ -159,7 +159,7 @@ flowchart LR
|
||||||
- Ist-Implementierung Prompts/UI: `AI_PROMPT_SYSTEM_SPEC.md`
|
- Ist-Implementierung Prompts/UI: `AI_PROMPT_SYSTEM_SPEC.md`
|
||||||
- Zugriffsrecht Admin-Prompts: `ACCESS_LAYER_ENDPOINT_AUDIT.md`
|
- Zugriffsrecht Admin-Prompts: `ACCESS_LAYER_ENDPOINT_AUDIT.md`
|
||||||
- Retrieval-Profile: `.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`
|
- Retrieval-Profile: `.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`
|
||||||
- Übungs-KI-Codepfad: `backend/exercise_ai.py`, `backend/prompt_resolver.py`, `backend/ai_prompt_runtime.py`, `backend/ai_prompt_job.py`
|
- Übungs-KI-Codepfad: `backend/exercise_ai.py`, `backend/prompt_resolver.py`, `backend/ai_prompt_runtime.py`, `backend/ai_prompt_context.py`, `backend/ai_prompt_job.py`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
86
backend/ai_prompt_context.py
Normal file
86
backend/ai_prompt_context.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
"""
|
||||||
|
Gemeinsame Pydantic-Modelle fuer Uebungs-KI-Kontext (Formularfelder → Prompt-Platzhalter).
|
||||||
|
|
||||||
|
Keine Imports aus exercise_ai — vermeidet Zirkelimporte mit ai_prompt_job / exercise_ai.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ExerciseFormAiFocusRow(BaseModel):
|
||||||
|
"""Fokusbereich fuer Skill-Retrieval (ai_skill_retrieval_profiles)."""
|
||||||
|
|
||||||
|
focus_area_id: int = Field(..., ge=1)
|
||||||
|
is_primary: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExerciseFormAiPromptContext(BaseModel):
|
||||||
|
"""
|
||||||
|
Inhaltliche Eingabe fuer Uebungs-Prompts (Kurzfassung / Skill-Vorschlaege).
|
||||||
|
|
||||||
|
Wird genutzt von Admin-Prompt-Vorschau und POST /exercises/ai/suggest (via Mapping).
|
||||||
|
"""
|
||||||
|
|
||||||
|
title: Optional[str] = ""
|
||||||
|
goal: Optional[str] = None
|
||||||
|
execution: Optional[str] = None
|
||||||
|
focus_hint: Optional[str] = None
|
||||||
|
focus_areas_context: Optional[List[ExerciseFormAiFocusRow]] = None
|
||||||
|
|
||||||
|
def focus_area_tuples(self) -> Optional[List[Tuple[int, bool]]]:
|
||||||
|
if not self.focus_areas_context:
|
||||||
|
return None
|
||||||
|
return [(int(x.focus_area_id), bool(x.is_primary)) for x in self.focus_areas_context]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_api_suggest(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
goal: Optional[str] = None,
|
||||||
|
execution: Optional[str] = None,
|
||||||
|
focus_area_hint: Optional[str] = None,
|
||||||
|
focus_areas_context: Optional[Sequence[ExerciseFormAiFocusRow]] = None,
|
||||||
|
) -> ExerciseFormAiPromptContext:
|
||||||
|
"""Mappt Felder aus POST /exercises/ai/suggest (focus_area_hint → focus_hint)."""
|
||||||
|
hint = (focus_area_hint or "").strip() or None
|
||||||
|
return cls(
|
||||||
|
title=(title or "").strip(),
|
||||||
|
goal=goal,
|
||||||
|
execution=execution,
|
||||||
|
focus_hint=hint,
|
||||||
|
focus_areas_context=list(focus_areas_context) if focus_areas_context else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_focus_tuples(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
title: str = "",
|
||||||
|
goal: Optional[str] = None,
|
||||||
|
execution: Optional[str] = None,
|
||||||
|
focus_hint: Optional[str] = None,
|
||||||
|
focus_tuples: Optional[Sequence[Tuple[int, bool]]] = None,
|
||||||
|
) -> ExerciseFormAiPromptContext:
|
||||||
|
rows = None
|
||||||
|
if focus_tuples:
|
||||||
|
rows = [
|
||||||
|
ExerciseFormAiFocusRow(focus_area_id=int(fid), is_primary=bool(prim))
|
||||||
|
for fid, prim in focus_tuples
|
||||||
|
]
|
||||||
|
return cls(
|
||||||
|
title=(title or "").strip(),
|
||||||
|
goal=goal,
|
||||||
|
execution=execution,
|
||||||
|
focus_hint=(focus_hint or "").strip() or None,
|
||||||
|
focus_areas_context=rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ExerciseFormAiFocusRow",
|
||||||
|
"ExerciseFormAiPromptContext",
|
||||||
|
]
|
||||||
|
|
@ -1,47 +1,16 @@
|
||||||
"""
|
"""
|
||||||
KI-Prompt Jobs: validierter Kontext (Pydantic) + Zugriff auf gemeinsame Resolver.
|
KI-Prompt Jobs: Resolver + oeffentliche Fassade fuer Uebungs-KI-Aufrufe.
|
||||||
|
|
||||||
Importiert exercise_ai nur fuer Platzhalter-Builder — Router importieren dieses Modul oder exercise_ai,
|
Importiert exercise_ai fuer Platzhalter-Builder und OpenRouter-Orchestrierung.
|
||||||
nicht umgekehrt von exercise_ai nach ai_prompt_job (Zirkel vermeiden).
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Any, Dict
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
from ai_prompt_context import ExerciseFormAiFocusRow, ExerciseFormAiPromptContext
|
||||||
from exercise_ai import build_exercise_placeholder_variables
|
from exercise_ai import build_exercise_placeholder_variables
|
||||||
|
|
||||||
|
|
||||||
class ExerciseFormAiFocusRow(BaseModel):
|
|
||||||
"""Fokusbereich fuer Skill-Retrieval-Kontext (wie ExerciseAiFocusCtx / Admin-Preview)."""
|
|
||||||
|
|
||||||
focus_area_id: int = Field(..., ge=1)
|
|
||||||
is_primary: Optional[bool] = False
|
|
||||||
|
|
||||||
|
|
||||||
class ExerciseFormAiPromptContext(BaseModel):
|
|
||||||
"""
|
|
||||||
Eingabe fuer Uebungsbezogene Prompts (Kurzfassung / Skill-JSON-Vorschlag).
|
|
||||||
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] = ""
|
|
||||||
goal: Optional[str] = None
|
|
||||||
execution: Optional[str] = None
|
|
||||||
focus_hint: Optional[str] = None
|
|
||||||
focus_areas_context: Optional[List[ExerciseFormAiFocusRow]] = None
|
|
||||||
|
|
||||||
def focus_area_tuples(self) -> Optional[List[Tuple[int, bool]]]:
|
|
||||||
if not self.focus_areas_context:
|
|
||||||
return None
|
|
||||||
return [(int(x.focus_area_id), bool(x.is_primary)) for x in self.focus_areas_context]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_exercise_form_variables(cur, slug: str, ctx: ExerciseFormAiPromptContext) -> Dict[str, str]:
|
def resolve_exercise_form_variables(cur, slug: str, ctx: ExerciseFormAiPromptContext) -> Dict[str, str]:
|
||||||
"""Baut die Mustache-Map fuer exercise_summary / exercise_skill_suggestions."""
|
"""Baut die Mustache-Map fuer exercise_summary / exercise_skill_suggestions."""
|
||||||
return build_exercise_placeholder_variables(
|
return build_exercise_placeholder_variables(
|
||||||
|
|
@ -55,8 +24,31 @@ def resolve_exercise_form_variables(cur, slug: str, ctx: ExerciseFormAiPromptCon
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_exercise_form_ai_suggestion(
|
||||||
|
cur,
|
||||||
|
ctx: ExerciseFormAiPromptContext,
|
||||||
|
*,
|
||||||
|
want_summary: bool,
|
||||||
|
want_skills: bool,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Fuehrt Uebungs-KI aus (OpenRouter) — ein Einstieg fuer Router und kuenftige Jobs.
|
||||||
|
|
||||||
|
``ctx`` = Formularinhalt; ``want_*`` = welche Prompt-Slugs angefragt werden.
|
||||||
|
"""
|
||||||
|
from exercise_ai import run_exercise_ai_suggestion
|
||||||
|
|
||||||
|
return run_exercise_ai_suggestion(
|
||||||
|
cur,
|
||||||
|
form_ctx=ctx,
|
||||||
|
want_summary=want_summary,
|
||||||
|
want_skills=want_skills,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ExerciseFormAiFocusRow",
|
"ExerciseFormAiFocusRow",
|
||||||
"ExerciseFormAiPromptContext",
|
"ExerciseFormAiPromptContext",
|
||||||
"resolve_exercise_form_variables",
|
"resolve_exercise_form_variables",
|
||||||
|
"run_exercise_form_ai_suggestion",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from openrouter_chat import (
|
||||||
openrouter_chat_completion,
|
openrouter_chat_completion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ai_prompt_context import ExerciseFormAiPromptContext
|
||||||
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
|
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
|
||||||
|
|
||||||
_LOGGER = logging.getLogger("shinkan.exercise_ai")
|
_LOGGER = logging.getLogger("shinkan.exercise_ai")
|
||||||
|
|
@ -682,16 +683,18 @@ def _require_openrouter_key() -> str:
|
||||||
def run_exercise_ai_suggestion(
|
def run_exercise_ai_suggestion(
|
||||||
cur,
|
cur,
|
||||||
*,
|
*,
|
||||||
title: Optional[str],
|
form_ctx: ExerciseFormAiPromptContext,
|
||||||
goal: Optional[str],
|
|
||||||
execution: Optional[str],
|
|
||||||
focus_area_hint: Optional[str],
|
|
||||||
focus_areas_context: Optional[Sequence[Tuple[int, bool]]] = None,
|
|
||||||
want_summary: bool,
|
want_summary: bool,
|
||||||
want_skills: bool,
|
want_skills: bool,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
key = _require_openrouter_key()
|
key = _require_openrouter_key()
|
||||||
|
|
||||||
|
title = form_ctx.title
|
||||||
|
goal = form_ctx.goal
|
||||||
|
execution = form_ctx.execution
|
||||||
|
focus_area_hint = form_ctx.focus_hint
|
||||||
|
focus_areas_context = form_ctx.focus_area_tuples()
|
||||||
|
|
||||||
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)
|
||||||
if not (g_plain.strip() or e_plain.strip()):
|
if not (g_plain.strip() or e_plain.strip()):
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from auth import require_auth
|
from auth import require_auth
|
||||||
from club_tenancy import is_superadmin
|
from club_tenancy import is_superadmin
|
||||||
from ai_prompt_job import ExerciseFormAiPromptContext, resolve_exercise_form_variables
|
from ai_prompt_context import ExerciseFormAiPromptContext
|
||||||
|
from ai_prompt_job import resolve_exercise_form_variables
|
||||||
from ai_prompt_runtime import render_ai_prompt_template_for_row
|
from ai_prompt_runtime import render_ai_prompt_template_for_row
|
||||||
from db import get_cursor, get_db, r2d
|
from db import get_cursor, get_db, r2d
|
||||||
from prompt_resolver import exercise_placeholder_catalog
|
from prompt_resolver import exercise_placeholder_catalog
|
||||||
|
|
@ -60,17 +61,8 @@ class AiPromptUpdateBody(BaseModel):
|
||||||
openrouter_model: Optional[str] = Field(None, max_length=200)
|
openrouter_model: Optional[str] = Field(None, max_length=200)
|
||||||
|
|
||||||
|
|
||||||
class AiPromptPreviewFocus(BaseModel):
|
class AiPromptPreviewBody(ExerciseFormAiPromptContext):
|
||||||
focus_area_id: int = Field(..., ge=1)
|
"""Preview-POST: gleiche Felder wie ExerciseFormAiPromptContext (focus_hint, nicht focus_area_hint)."""
|
||||||
is_primary: Optional[bool] = False
|
|
||||||
|
|
||||||
|
|
||||||
class AiPromptPreviewBody(BaseModel):
|
|
||||||
title: Optional[str] = ""
|
|
||||||
goal: Optional[str] = None
|
|
||||||
execution: Optional[str] = None
|
|
||||||
focus_hint: Optional[str] = None
|
|
||||||
focus_areas_context: Optional[List[AiPromptPreviewFocus]] = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/admin/ai-prompts/catalog/placeholders")
|
@router.get("/api/admin/ai-prompts/catalog/placeholders")
|
||||||
|
|
@ -228,8 +220,7 @@ def preview_ai_prompt(prompt_id: int, body: AiPromptPreviewBody, session: dict =
|
||||||
warn: Optional[str] = None
|
warn: Optional[str] = None
|
||||||
if slug in ("exercise_summary", "exercise_skill_suggestions"):
|
if slug in ("exercise_summary", "exercise_skill_suggestions"):
|
||||||
try:
|
try:
|
||||||
pf_ctx = ExerciseFormAiPromptContext.model_validate(body.model_dump())
|
vars_map = resolve_exercise_form_variables(cur, slug, body)
|
||||||
vars_map = resolve_exercise_form_variables(cur, slug, pf_ctx)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||||
elif slug == "pipeline":
|
elif slug == "pipeline":
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ from tenant_context import TenantContext, get_tenant_context, get_tenant_context
|
||||||
from media_storage import get_effective_media_root, library_storage_key, path_under_media_root
|
from media_storage import get_effective_media_root, library_storage_key, path_under_media_root
|
||||||
from media_rights import assert_rights_for_exercise_link, validate_rights_declaration, write_rights_declaration, update_rights_quick_fields
|
from media_rights import assert_rights_for_exercise_link, validate_rights_declaration, write_rights_declaration, update_rights_quick_fields
|
||||||
from media_legal_hold import assert_not_under_legal_hold
|
from media_legal_hold import assert_not_under_legal_hold
|
||||||
from exercise_ai import run_exercise_ai_suggestion
|
from ai_prompt_context import ExerciseFormAiFocusRow, ExerciseFormAiPromptContext
|
||||||
|
from ai_prompt_job import run_exercise_form_ai_suggestion
|
||||||
|
|
||||||
from exercise_rich_text import (
|
from exercise_rich_text import (
|
||||||
RICH_HTML_EXERCISE_FIELDS,
|
RICH_HTML_EXERCISE_FIELDS,
|
||||||
|
|
@ -358,11 +359,8 @@ class ExerciseMediaFromAsset(BaseModel):
|
||||||
media_type: Optional[str] = None
|
media_type: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ExerciseAiFocusCtx(BaseModel):
|
class ExerciseAiFocusCtx(ExerciseFormAiFocusRow):
|
||||||
"""Fokusbereich fuer Skill-Kataloggewichte (Migration 068 ai_skill_retrieval_profiles)."""
|
"""Alias fuer OpenAPI — identisch zu ExerciseFormAiFocusRow."""
|
||||||
|
|
||||||
focus_area_id: int = Field(..., ge=1)
|
|
||||||
is_primary: Optional[bool] = False
|
|
||||||
|
|
||||||
|
|
||||||
class ExerciseAiSuggestBody(BaseModel):
|
class ExerciseAiSuggestBody(BaseModel):
|
||||||
|
|
@ -370,7 +368,7 @@ class ExerciseAiSuggestBody(BaseModel):
|
||||||
goal: Optional[str] = Field(None, max_length=64000)
|
goal: Optional[str] = Field(None, max_length=64000)
|
||||||
execution: Optional[str] = Field(None, max_length=128000)
|
execution: Optional[str] = Field(None, max_length=128000)
|
||||||
focus_area_hint: Optional[str] = Field(None, max_length=1200)
|
focus_area_hint: Optional[str] = Field(None, max_length=1200)
|
||||||
focus_areas_context: Optional[list[ExerciseAiFocusCtx]] = Field(
|
focus_areas_context: Optional[list[ExerciseFormAiFocusRow]] = Field(
|
||||||
None,
|
None,
|
||||||
description="Optionale Reihenfolge Primär zuerst; steuert Katalogpriorisierung",
|
description="Optionale Reihenfolge Primär zuerst; steuert Katalogpriorisierung",
|
||||||
)
|
)
|
||||||
|
|
@ -383,6 +381,15 @@ class ExerciseAiSuggestBody(BaseModel):
|
||||||
raise ValueError("Mindestens include_summary oder include_skills aktivieren.")
|
raise ValueError("Mindestens include_summary oder include_skills aktivieren.")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def to_form_context(self) -> ExerciseFormAiPromptContext:
|
||||||
|
return ExerciseFormAiPromptContext.from_api_suggest(
|
||||||
|
title=self.title,
|
||||||
|
goal=self.goal,
|
||||||
|
execution=self.execution,
|
||||||
|
focus_area_hint=self.focus_area_hint,
|
||||||
|
focus_areas_context=self.focus_areas_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExerciseAiRegenerateBody(BaseModel):
|
class ExerciseAiRegenerateBody(BaseModel):
|
||||||
"""Welche Artefakte neu angefragt werden sollen."""
|
"""Welche Artefakte neu angefragt werden sollen."""
|
||||||
|
|
@ -2306,17 +2313,9 @@ def exercise_ai_suggest_endpoint(
|
||||||
_ = tenant.profile_id
|
_ = tenant.profile_id
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
fctx = None
|
payload = run_exercise_form_ai_suggestion(
|
||||||
if body.focus_areas_context:
|
|
||||||
fctx = [(x.focus_area_id, bool(x.is_primary)) for x in body.focus_areas_context]
|
|
||||||
|
|
||||||
payload = run_exercise_ai_suggestion(
|
|
||||||
cur,
|
cur,
|
||||||
title=(body.title or "").strip(),
|
body.to_form_context(),
|
||||||
goal=body.goal,
|
|
||||||
execution=body.execution,
|
|
||||||
focus_area_hint=(body.focus_area_hint or "").strip() or None,
|
|
||||||
focus_areas_context=fctx,
|
|
||||||
want_summary=body.include_summary,
|
want_summary=body.include_summary,
|
||||||
want_skills=body.include_skills,
|
want_skills=body.include_skills,
|
||||||
)
|
)
|
||||||
|
|
@ -2344,13 +2343,16 @@ def exercise_ai_regenerate_endpoint(
|
||||||
focus = _focus_area_hint_from_detail(exercise)
|
focus = _focus_area_hint_from_detail(exercise)
|
||||||
fctx = _focus_areas_ai_ctx_from_detail(exercise)
|
fctx = _focus_areas_ai_ctx_from_detail(exercise)
|
||||||
|
|
||||||
payload = run_exercise_ai_suggestion(
|
ctx = ExerciseFormAiPromptContext.from_focus_tuples(
|
||||||
cur,
|
|
||||||
title=str(exercise.get("title") or "").strip(),
|
title=str(exercise.get("title") or "").strip(),
|
||||||
goal=exercise.get("goal"),
|
goal=exercise.get("goal"),
|
||||||
execution=exercise.get("execution"),
|
execution=exercise.get("execution"),
|
||||||
focus_area_hint=focus or None,
|
focus_hint=focus or None,
|
||||||
focus_areas_context=fctx or None,
|
focus_tuples=fctx or None,
|
||||||
|
)
|
||||||
|
payload = run_exercise_form_ai_suggestion(
|
||||||
|
cur,
|
||||||
|
ctx,
|
||||||
want_summary=want_summary,
|
want_summary=want_summary,
|
||||||
want_skills=want_skills,
|
want_skills=want_skills,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.161"
|
APP_VERSION = "0.8.162"
|
||||||
BUILD_DATE = "2026-05-31"
|
BUILD_DATE = "2026-05-31"
|
||||||
DB_SCHEMA_VERSION = "20260531070"
|
DB_SCHEMA_VERSION = "20260531070"
|
||||||
|
|
||||||
|
|
@ -20,13 +20,14 @@ MODULE_VERSIONS = {
|
||||||
"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.3", # Migration 070: openrouter_model; PUT/Liste/Detail
|
"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.2.0", # run_exercise_form_ai_suggestion; Kontext in ai_prompt_context
|
||||||
|
"ai_prompt_context": "0.1.0", # ExerciseFormAiPromptContext — Formularfelder fuer Uebungs-Prompts
|
||||||
"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.3", # exercise_ai: OpenRouter-Modell pro Prompt-Slug; Response models_by_slug
|
"exercises": "2.31.4", # ai/suggest + regenerate: ExerciseFormAiPromptContext via run_exercise_form_ai_suggestion
|
||||||
"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 +42,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.162",
|
||||||
|
"date": "2026-05-31",
|
||||||
|
"changes": [
|
||||||
|
"KI Prompt P1 abgeschlossen: ai_prompt_context (Formular-Kontext), run_exercise_form_ai_suggestion als gemeinsamer Einstieg;",
|
||||||
|
"POST /exercises/ai/suggest und regenerate bauen ExerciseFormAiPromptContext; Admin-Vorschau nutzt dasselbe Modell.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.161",
|
"version": "0.8.161",
|
||||||
"date": "2026-05-31",
|
"date": "2026-05-31",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||||
|
|
||||||
**Stand:** 2026-05-30
|
**Stand:** 2026-05-31
|
||||||
**App-Version / DB-Schema:** App **`0.8.160`** (u. a. KI Prompt P1: `load_and_render_ai_prompt`, `ai_prompt_job`); Zielarchitektur-Doku weiterhin **`.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md`**. DB: maßgeblich **`backend/version.py`** → `APP_VERSION`, `DB_SCHEMA_VERSION` (aktuell `20260530069`).
|
**App-Version / DB-Schema:** App **`0.8.162`** (KI Prompt P1: gemeinsamer Formular-Kontext `ExerciseFormAiPromptContext`, `run_exercise_form_ai_suggestion`); Zielarchitektur **`.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md`**. DB: **`backend/version.py`** → `DB_SCHEMA_VERSION` (aktuell `20260531070`).
|
||||||
|
|
||||||
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
||||||
|
|
||||||
|
|
@ -89,12 +89,12 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
- **Varianten:** Speichern in der **Aktionsleiste** persistiert zuerst geänderte Varianten (`persistPendingVariantChanges`), dann Übungs-Stammdaten; „Variante anlegen“ als `type="button"` ohne verschachteltes Formular (`createVariantFromDraft`)
|
- **Varianten:** Speichern in der **Aktionsleiste** persistiert zuerst geänderte Varianten (`persistPendingVariantChanges`), dann Übungs-Stammdaten; „Variante anlegen“ als `type="button"` ohne verschachteltes Formular (`createVariantFromDraft`)
|
||||||
- **Governance (Übungen):** Owner = `created_by`; Bearbeiten = Ersteller, Plattform-Admin oder `can_plan_in_club` bei `visibility=club`; Löschen `club` = nur `club_admin`; Details **`FEATURES_DELIVERED_2026-Q2.md`** §16, **`EXERCISES_API_SPEC.md`** Permissions
|
- **Governance (Übungen):** Owner = `created_by`; Bearbeiten = Ersteller, Plattform-Admin oder `can_plan_in_club` bei `visibility=club`; Löschen `club` = nur `club_admin`; Details **`FEATURES_DELIVERED_2026-Q2.md`** §16, **`EXERCISES_API_SPEC.md`** Permissions
|
||||||
|
|
||||||
### 2.8 KI Assistenz Übungen & Skill-Katalog-Retrieval (Stand **0.8.160**)
|
### 2.8 KI Assistenz Übungen & Skill-Katalog-Retrieval (Stand **0.8.162**)
|
||||||
|
|
||||||
- **Zielarchitektur (Pflicht fuer Erweiterungen):** `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` — Kontext-Arten, Composition, Einbindung Planung/Rahmen; Phasenplan P0–P4.
|
- **Zielarchitektur (Pflicht fuer Erweiterungen):** `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` — Kontext-Arten, Composition, Einbindung Planung/Rahmen; Phasenplan P0–P4.
|
||||||
- **Doku:** Umsetzung `.claude/docs/working/AI_EXERCISE_IMPLEMENTATION_PLAN.md`; Profil-/JSON-Konzept `.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`; Ist-Prompt/UI **`AI_PROMPT_SYSTEM_SPEC.md`**; API-Felder **`KI_FEATURES_SPEC.md`** §5.2
|
- **Doku:** Umsetzung `.claude/docs/working/AI_EXERCISE_IMPLEMENTATION_PLAN.md`; Profil-/JSON-Konzept `.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`; Ist-Prompt/UI **`AI_PROMPT_SYSTEM_SPEC.md`**; API-Felder **`KI_FEATURES_SPEC.md`** §5.2
|
||||||
- **Runtime / Job:** **`ai_prompt_runtime`** (`load_and_render_ai_prompt`, `AiPromptUnavailableError`, Rendern aus DB-Zeile fuer Vorschau); **`ai_prompt_job`** — Pydantic **`ExerciseFormAiPromptContext`**, **`resolve_exercise_form_variables`** (Admin-Preview + Schnittstelle fuer weitere Router); **`exercise_ai`** laedt/rendert ueber Laufzeit und ruft OpenRouter
|
- **Kontext / Job:** **`ai_prompt_context`** — **`ExerciseFormAiPromptContext`** (Titel, Ziel, Durchführung, Fokus — ein Modell fuer Admin-Vorschau und Übungs-API); **`ai_prompt_job`** — **`run_exercise_form_ai_suggestion`**, **`resolve_exercise_form_variables`**; **`ai_prompt_runtime`** — Laden/Rendern aus DB; **`exercise_ai`** — OpenRouter nach Rendern
|
||||||
- **DB:** Migration **`067`** **`ai_prompts`** (Slug **`exercise_summary`**, **`exercise_skill_suggestions`** — müssen **aktiv** sein); Migration **`069`** setzt **`default_template`** wo leer; Migration **`068`** **`ai_skill_retrieval_profiles`** (Seed Standard + ggf. Gewaltschutz-Fokus)
|
- **DB:** Migration **`067`** **`ai_prompts`**; **`069`** **`default_template`**; **`068`** **`ai_skill_retrieval_profiles`**; **`070`** **`openrouter_model`** (optional pro Prompt, Fallback **`OPENROUTER_MODEL`**)
|
||||||
- **`exercise_ai`:** Gewichtungen, Kategorie‑Anteil‑Caps (~Token), Keyword-Patches aus Ziel/Durchführung (z. B. Rollenspiel vs. Befreiung/Haltegriff)
|
- **`exercise_ai`:** Gewichtungen, Kategorie‑Anteil‑Caps (~Token), Keyword-Patches aus Ziel/Durchführung (z. B. Rollenspiel vs. Befreiung/Haltegriff)
|
||||||
- **API:** `POST /api/exercises/ai/suggest` optional **`focus_areas_context`**; **`POST …/ai/regenerate`** nutzt gespeicherte `exercise_focus_areas` — **Pflege:** Superadmin **`/api/admin/ai-skill-retrieval-profiles*`** (`routers/ai_skill_retrieval_admin.py`), **`/api/admin/ai-prompts*`** (`routers/ai_prompts_admin.py`), UI **`/admin/ai-prompts`**
|
- **API:** `POST /api/exercises/ai/suggest` optional **`focus_areas_context`**; **`POST …/ai/regenerate`** nutzt gespeicherte `exercise_focus_areas` — **Pflege:** Superadmin **`/api/admin/ai-skill-retrieval-profiles*`** (`routers/ai_skill_retrieval_admin.py`), **`/api/admin/ai-prompts*`** (`routers/ai_prompts_admin.py`), UI **`/admin/ai-prompts`**
|
||||||
- **Diagnose bei leerem Dialog / Fehlern:** Umgebungsvariable **`SHINKAN_AI_DEBUG=1`** auf der API; in den Logs dann **`AI_DEBUG`** (`shinkan.exercise_ai`) und **`[AI_DEBUG/openrouter]`** (`shinkan.openrouter`) mit Prompt-Längen, Token-Zahlen und ggf. JSON-Parse-Anfang
|
- **Diagnose bei leerem Dialog / Fehlern:** Umgebungsvariable **`SHINKAN_AI_DEBUG=1`** auf der API; in den Logs dann **`AI_DEBUG`** (`shinkan.exercise_ai`) und **`[AI_DEBUG/openrouter]`** (`shinkan.openrouter`) mit Prompt-Längen, Token-Zahlen und ggf. JSON-Parse-Anfang
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user