refactor
This commit is contained in:
@@ -3,6 +3,7 @@ import json
|
||||
import logging
|
||||
import shutil
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
@@ -19,6 +20,7 @@ from database import (
|
||||
delete_topic_pipeline, delete_source, get_guide_content, delete_guide_content,
|
||||
get_sub_artefakte, kanban_reset, delete_guide_board,
|
||||
get_practice_progress, upsert_practice_progress, sub_levels_norm, subs_per_level_norm,
|
||||
list_runs, get_db,
|
||||
)
|
||||
from textkit import _norm_title
|
||||
from blocks import generate_blocks, cancel_blocks, blocks_status, active_blocks, reset_blocks, load_source, load_overview, subblocks_title, subblocks_frei, load_question_pattern, load_question_pattern_free, _blocks_files
|
||||
@@ -72,6 +74,18 @@ async def get_stats():
|
||||
return {"topics": len(topics), "formats": formats_stats(guides, levels)}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
await (await get_db()).execute("SELECT 1")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/runs")
|
||||
async def get_runs(topic: str, limit: int = 10):
|
||||
"""Lauf-Historie (Blocks + Guide): Zeitspanne, Agenten, Tokens, Fehler je run_id."""
|
||||
return {"runs": await list_runs(topic, limit)}
|
||||
|
||||
|
||||
@router.get("/topics/progress")
|
||||
async def topic_progress(topic: str):
|
||||
"""Completion status per format + topic completion — for unlocking the next expansion stage."""
|
||||
@@ -89,14 +103,28 @@ async def add_topic(req: TopicCreateRequest):
|
||||
|
||||
@router.delete("/topics")
|
||||
async def remove_topic(topic: str):
|
||||
guides = [g for g in await list_guides() if g["topic"] == topic]
|
||||
status = await blocks_status(topic)
|
||||
if status["generating"] or any(g["status"] == "generating" for g in guides):
|
||||
raise HTTPException(409, "Generierung läuft — erst abbrechen")
|
||||
await delete_topic(topic)
|
||||
await delete_block_data(topic)
|
||||
await delete_topic_pipeline(topic)
|
||||
await delete_source(topic) # topic config (DB) — removed together with the topic
|
||||
await delete_guide_content(topic)
|
||||
shutil.rmtree(topic_dir(topic), ignore_errors=True)
|
||||
# guides/Board/Kanban mitlöschen — GET /topics leitet Topics aus guides ab,
|
||||
# sonst taucht das gelöschte Topic sofort wieder auf
|
||||
for g in guides:
|
||||
await delete_guide(g["id"])
|
||||
await delete_guide_board(topic)
|
||||
await kanban_reset(topic)
|
||||
import qa
|
||||
shutil.rmtree(qa.QA_DIR / topic, ignore_errors=True) # QA reports belong to the topic
|
||||
# rmtree (potenziell große Topic-Ordner) in den Threadpool — der Event-Loop bedient
|
||||
# parallel laufende Flows/Polls, blockierendes Datei-I/O friert die alle ein
|
||||
def _wipe():
|
||||
shutil.rmtree(topic_dir(topic), ignore_errors=True)
|
||||
shutil.rmtree(qa.QA_DIR / topic, ignore_errors=True) # QA reports belong to the topic
|
||||
await asyncio.to_thread(_wipe)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -325,9 +353,10 @@ async def blocks_completeness(topic: str):
|
||||
counts = await kanban_stage_counts(topic)
|
||||
inv = counts.get("inventory", {})
|
||||
blocks = await list_blocks(topic, status="consensus")
|
||||
subs = 0
|
||||
for b in blocks:
|
||||
subs += sum(1 for s in await list_subblocks(topic, b["title_norm"]) if s["status"] == "consensus")
|
||||
# ein Query statt N+1 (pro Block ein list_subblocks) — in Python nach consensus zählen
|
||||
consensus_blocks = {b["title_norm"] for b in blocks}
|
||||
subs = sum(1 for s in await list_subblocks(topic)
|
||||
if s["status"] == "consensus" and s["block_norm"] in consensus_blocks)
|
||||
ziele = await list_lernziele(topic)
|
||||
dead = sum(v for board in counts.values() for s, v in board.items() if s == "dead")
|
||||
degradiert = ueberstimmt = 0
|
||||
@@ -466,15 +495,26 @@ async def block_chat_route(req: BlockChatRequest):
|
||||
|
||||
# Serialize ratings per (topic, block) — otherwise two simultaneous ratings would
|
||||
# overwrite the absolute score with a stale base (race).
|
||||
_check_locks: dict[tuple[str, str], asyncio.Lock] = {}
|
||||
_check_locks: dict[tuple[str, str], tuple[asyncio.Lock, list]] = {}
|
||||
|
||||
|
||||
def _check_lock(topic: str, block: str) -> asyncio.Lock:
|
||||
@asynccontextmanager
|
||||
async def _check_lock(topic: str, block: str):
|
||||
"""Per-(topic,block)-Lock mit Refcount, das den Eintrag nach dem letzten Nutzer
|
||||
entfernt — die Map wuchs sonst unbegrenzt (ein Lock pro je geprüftem Block)."""
|
||||
key = (topic, block)
|
||||
lock = _check_locks.get(key)
|
||||
if lock is None:
|
||||
lock = _check_locks[key] = asyncio.Lock()
|
||||
return lock
|
||||
entry = _check_locks.get(key)
|
||||
if entry is None:
|
||||
entry = _check_locks[key] = (asyncio.Lock(), [0])
|
||||
lock, ref = entry
|
||||
ref[0] += 1
|
||||
try:
|
||||
async with lock:
|
||||
yield
|
||||
finally:
|
||||
ref[0] -= 1
|
||||
if ref[0] == 0 and _check_locks.get(key) is entry:
|
||||
del _check_locks[key]
|
||||
|
||||
|
||||
def _basis(state: dict, question: str) -> tuple[int, bool]:
|
||||
|
||||
Reference in New Issue
Block a user