This commit is contained in:
team3
2026-07-02 03:05:57 +02:00
parent afa8b36105
commit 41c9f29a37
38 changed files with 4671 additions and 2634 deletions

View File

@@ -7,7 +7,7 @@ from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response
from agents import provider_available
from agents import active_agents, provider_available
from config import PROJECTS_DIR, UNI_DIR, PROVIDERS
from database import (
create_guide, delete_guide, get_guide, list_guides,
@@ -18,19 +18,20 @@ from database import (
set_block_score_and_streak, set_block_completed,
delete_block_data, delete_block_progress, subs_per_level, subs_per_level_raw,
delete_topic_pipeline, delete_source, get_guide_content, delete_guide_content,
get_sub_artefakte,
get_sub_artefakte, kanban_reset, delete_guide_board,
)
from blocks import generate_blocks, cancel_blocks, blocks_status, active_blocks, reset_blocks, reset_blocks_ab_step, load_source, load_overview, subblocks_title, subblocks_frei, load_question_pattern, load_question_pattern_free
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
from board_inventory import add_research_agent, board_snapshot, requeue_dead, reset_board_from_stage
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
from learning import block_chat, block_discussion, create_block_element, exam_rating, exam_rating_fast, exam_question, exam_question_variant, generate_quiz, generate_gapchoice, generate_gaptext, check_gaptext, hurdles_distractor_block, compute_score, floor_from_score, level_from_score, cap_final, cap_aktuell, freie_level, thresholds, points_delta, cap_followup
from guide import generate_guide, guide_slot_files, guide_done_step, block_pruefen, block_adopt, content_fuer_level
from guide import generate_guide, guide_slot_files, block_pruefen, block_adopt, content_fuer_level
from pipeline import cancel_guide
from rules import FORMATE, formats_stats, guide_lock, ist_completed, load_learnstate, topic_completed
from models import (
GuideCreateRequest, GuideResponse,
TopicCreateRequest,
BlocksCreateRequest, BlocksResetStepRequest, BlocksStatusResponse,
GuideChatRequest, GuideChatResponse,
BlocksCreateRequest, BlocksResetStageRequest, BlocksStatusResponse,
GuideBoardResetRequest, GuideChatRequest, GuideChatResponse,
ElementCreateRequest, ElementChatRequest, ElementChatResponse, ElementResponse,
ElementUpdateRequest, ElementCheckRequest, ElementCheckResponse, ElementStyleResponse,
ElementRefineRequest, ElementRefineResponse,
@@ -164,10 +165,55 @@ async def create_blocks(req: BlocksCreateRequest):
raise HTTPException(400, "Link must start with http:// or https://.")
qp.parent.mkdir(parents=True, exist_ok=True)
atomic_write_json(qp, {"type": type, "location": location, "spec": req.instructions.strip()})
asyncio.create_task(generate_blocks(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase, ab_step=req.ab_step, to_step=req.to_step))
asyncio.create_task(generate_blocks(topic, req.instructions.strip(), req.provider, research=req.research))
return {"ok": True}
@router.get("/blocks/board")
async def get_blocks_board(topic: str):
"""Live kanban board: columns with counts + newest cards, dead-letter, agents."""
snap = await board_snapshot(topic)
status = await blocks_status(topic)
snap["generating"] = status["generating"]
snap["progress"] = status["progress"]
snap["error"] = status["error"]
snap["agents"] = [{"label": a["key"].removeprefix(f"blocks-{topic}-"), "runtime": a["runtime"]}
for a in active_agents(f"blocks-{topic}-")]
return snap
@router.get("/blocks/agents")
async def get_blocks_agents(topic: str):
return [{"label": a["key"].removeprefix(f"blocks-{topic}-"), "runtime": a["runtime"]}
for a in active_agents(f"blocks-{topic}-")]
@router.post("/blocks/research")
async def add_blocks_research(topic: str, provider: str = "claude"):
"""Attach one more research agent — to the live flow, or attach-or-start."""
if add_research_agent(topic):
return {"ok": True, "attached": True}
if (await blocks_status(topic))["generating"]:
return {"ok": False, "status": "starting"} # flow is booting, try again shortly
asyncio.create_task(generate_blocks(topic, "", provider, research=True))
return {"ok": True, "attached": False}
@router.post("/blocks/reset-stage")
async def reset_blocks_stage(req: BlocksResetStageRequest):
"""Reset cards from a column onward back to that column (no regeneration)."""
topic = req.topic.strip()
if (await blocks_status(topic))["generating"]:
return {"ok": True, "status": "generating"} # don't interfere with a running generation
moved = await reset_board_from_stage(topic, req.board, req.stage, _blocks_files(topic))
return {"ok": True, "moved": moved}
@router.post("/blocks/requeue-dead")
async def requeue_blocks_dead(topic: str):
return {"ok": True, "requeued": await requeue_dead(topic)}
@router.post("/blocks/cancel")
async def cancel_blocks_route(topic: str):
if not cancel_blocks(topic):
@@ -179,15 +225,7 @@ async def cancel_blocks_route(topic: str):
async def remove_blocks(topic: str):
reset_blocks(topic) # Files: crawl + triage + inventory…questions gone; source.json stays
await delete_topic_pipeline(topic) # DB: blocks area gone; topic config (source) stays
return {"ok": True}
@router.post("/blocks/reset-step")
async def reset_blocks_step(req: BlocksResetStepRequest):
topic = req.topic.strip()
if (await blocks_status(topic))["generating"]:
return {"ok": True, "status": "generating"} # don't interfere with a running generation
await reset_blocks_ab_step(topic, req.ab_step)
await kanban_reset(topic) # kanban cards + cluster membership gone
return {"ok": True}
@@ -509,9 +547,46 @@ async def guide_locks(topic: str):
@router.get("/guides/steps")
async def guide_steps(topic: str):
"""Highest fully completed step index per format (artifact-based, -1 = none).
Drives the clickable step bubbles (like the blocks phases)."""
return {fmt: guide_done_step(guide_content_path(topic, fmt)) for fmt in ("Guide", "FullGuide", "Rest")}
"""Highest fully completed stage index per format (card-based, -1 = none).
Content file present (legacy without cards) → everything done."""
import guide_board
out = {}
for fmt in ("Guide", "FullGuide", "Rest"):
step = await guide_board.done_step(topic, fmt)
if step < 0 and guide_content_path(topic, fmt).exists():
step = len(guide_board.GUIDE_STAGES)
out[fmt] = step
return out
@router.get("/guides/board")
async def get_guide_board(topic: str, format: str = "Guide"):
"""Live guide board: columns with counts + cards (rounds, covered objectives), agents."""
import guide_board
snap = await guide_board.board_snapshot(topic, format)
guide = next((g for g in await list_guides()
if g["topic"] == topic and g["format"] == format), None)
snap["generating"] = bool(guide and guide["status"] in ("queued", "generating"))
snap["guide_id"] = guide["id"] if guide else None
snap["progress"] = guide.get("progress") if guide else None
snap["error"] = guide.get("error_msg") if guide else None
prefix = f"{guide['id']}-" if guide else "-"
snap["agents"] = [{"label": a["key"].removeprefix(prefix), "runtime": a["runtime"]}
for a in active_agents(prefix)]
return snap
@router.post("/guides/board/reset")
async def reset_guide_board(req: GuideBoardResetRequest):
"""Reset cards from a stage onward — without generation (pendant to blocks reset-stage)."""
import guide_board
topic = req.topic.strip()
guide = next((g for g in await list_guides()
if g["topic"] == topic and g["format"] == req.format), None)
if guide and guide["status"] in ("queued", "generating"):
return {"ok": True, "status": "generating"}
moved = await guide_board.reset_from_stage(topic, req.format, req.ab_stage)
return {"ok": True, "moved": moved}
@router.get("/guides/{guide_id}", response_model=GuideResponse)
@@ -680,6 +755,7 @@ async def remove(guide_id: str, slots: bool = False):
rest = [g for g in await list_guides() if g["topic"] == guide["topic"] and g["format"] == guide["format"]]
if not rest:
await delete_guide_content(guide["topic"], guide["format"])
await delete_guide_board(guide["topic"], guide["format"]) # board cards + lernziele
content = guide_content_path(guide["topic"], guide["format"])
if slots or content.exists():
for p in guide_slot_files(content):