update
This commit is contained in:
139
backend/agents.py
Normal file
139
backend/agents.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Provider-Schicht: führt Agent-Aufrufe über die Claude-CLI oder OpenCode (MiniMax) aus.
|
||||
|
||||
Beide Runner sind unabhängig. Fehlt ein Binary/Key, schlägt nur der
|
||||
jeweilige Provider fehl — der andere läuft unverändert weiter.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from config import PROVIDERS, DEFAULT_PROVIDER
|
||||
|
||||
_active_processes: dict[str, asyncio.subprocess.Process] = {}
|
||||
|
||||
# Capability → Claude --allowedTools
|
||||
_CLAUDE_TOOLS = {
|
||||
"full": "Write,Bash,Read,WebSearch,WebFetch",
|
||||
"files": "Read,Bash,Write",
|
||||
"read": "Read",
|
||||
"none": None,
|
||||
}
|
||||
|
||||
# Capability → OpenCode-Agent (Tool-Rechte in dev-ops/opencode.json definiert)
|
||||
_OPENCODE_AGENTS = {
|
||||
"full": "full",
|
||||
"files": "files",
|
||||
"read": "readonly",
|
||||
"none": "text",
|
||||
}
|
||||
|
||||
|
||||
def provider_available(provider: str) -> bool:
|
||||
cfg = PROVIDERS.get(provider)
|
||||
if not cfg:
|
||||
return False
|
||||
if shutil.which(cfg["cli"]) is None:
|
||||
return False
|
||||
env_key = cfg.get("env_key")
|
||||
if env_key and not os.environ.get(env_key):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def kill_process(agent_key: str) -> None:
|
||||
process = _active_processes.get(agent_key)
|
||||
if process and process.returncode is None:
|
||||
process.kill()
|
||||
|
||||
|
||||
async def run_agent(
|
||||
agent_key: str,
|
||||
prompt: str,
|
||||
timeout: int,
|
||||
provider: str = DEFAULT_PROVIDER,
|
||||
role: str = "fast",
|
||||
capabilities: str = "none",
|
||||
) -> tuple[int, str, str]:
|
||||
if provider not in PROVIDERS:
|
||||
return 1, "", f"Unbekannter Provider: {provider}"
|
||||
if shutil.which(PROVIDERS[provider]["cli"]) is None:
|
||||
return 1, "", f"CLI '{PROVIDERS[provider]['cli']}' nicht installiert (Provider: {provider})"
|
||||
timeout = int(timeout * PROVIDERS[provider].get("timeout_factor", 1))
|
||||
if provider == "minimax":
|
||||
return await _run_opencode(agent_key, prompt, timeout, role, capabilities)
|
||||
return await _run_claude_cli(agent_key, prompt, timeout, role, capabilities)
|
||||
|
||||
|
||||
async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None, timeout: int) -> tuple[int, str, str]:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_active_processes[agent_key] = process
|
||||
try:
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(input=stdin_data),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
raise
|
||||
return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
|
||||
finally:
|
||||
_active_processes.pop(agent_key, None)
|
||||
|
||||
|
||||
async def _run_claude_cli(agent_key: str, prompt: str, timeout: int, role: str, capabilities: str) -> tuple[int, str, str]:
|
||||
cfg = PROVIDERS["claude"]
|
||||
cmd = [cfg["cli"], "-p", "--model", cfg[role]]
|
||||
tools = _CLAUDE_TOOLS.get(capabilities)
|
||||
if tools:
|
||||
cmd += ["--allowedTools", tools]
|
||||
cmd += ["--dangerously-skip-permissions"]
|
||||
return await _communicate(agent_key, cmd, prompt.encode("utf-8"), timeout)
|
||||
|
||||
|
||||
async def _run_opencode(agent_key: str, prompt: str, timeout: int, role: str, capabilities: str) -> tuple[int, str, str]:
|
||||
cfg = PROVIDERS["minimax"]
|
||||
# Prompt über Tempdatei statt argv (ARG_MAX-Schutz bei großen Projekt-Prompts)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8", dir=tempfile.gettempdir()) as f:
|
||||
f.write(prompt)
|
||||
prompt_path = Path(f.name)
|
||||
# Positional-Message MUSS vor -f stehen: -f ist ein Array-Flag und
|
||||
# frisst sonst den Text als zweiten Dateinamen ("File not found").
|
||||
cmd = [
|
||||
cfg["cli"], "run",
|
||||
"Folge exakt den Anweisungen in der angehängten Datei. Sie sind der vollständige Auftrag.",
|
||||
"-m", cfg[role],
|
||||
"--agent", _OPENCODE_AGENTS.get(capabilities, "text"),
|
||||
"--dangerously-skip-permissions",
|
||||
"-f", str(prompt_path),
|
||||
]
|
||||
try:
|
||||
rc, stdout, stderr = await _communicate(agent_key, cmd, None, timeout)
|
||||
return rc, _clean_opencode_output(stdout), stderr
|
||||
finally:
|
||||
prompt_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
||||
|
||||
|
||||
def _clean_opencode_output(text: str) -> str:
|
||||
"""Entfernt ANSI-Codes und den führenden Banner ("> agent · modell")."""
|
||||
text = _ANSI_RE.sub("", text)
|
||||
lines = text.splitlines()
|
||||
while lines and (not lines[0].strip() or lines[0].lstrip().startswith(">")):
|
||||
lines.pop(0)
|
||||
return "\n".join(lines).strip()
|
||||
@@ -27,10 +27,23 @@ FORMAT_META = {
|
||||
AGENT_TIMEOUT = 3600
|
||||
|
||||
MAX_CONCURRENT_GENERATIONS = 6
|
||||
CLAUDE_CLI = "claude"
|
||||
|
||||
MODEL_GUIDE = "claude-opus-4-8[1m]"
|
||||
MODEL_BAUSTEIN_GEN = "claude-sonnet-4-6"
|
||||
MODEL_BAUSTEIN_REWORK = "claude-sonnet-4-6"
|
||||
MODEL_CHAT = "claude-sonnet-4-6"
|
||||
MODEL_PROJECT_INDEX = MODEL_BAUSTEIN_GEN
|
||||
# Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden.
|
||||
# Rollen: "guide" = große Generierung/Review, "fast" = Bausteine/Chat/Sortierung.
|
||||
DEFAULT_PROVIDER = "claude"
|
||||
PROVIDERS = {
|
||||
"claude": {
|
||||
"cli": "claude",
|
||||
"guide": "claude-opus-4-8[1m]",
|
||||
"fast": "claude-sonnet-4-6",
|
||||
"env_key": None, # Auth via CLAUDE_CODE_OAUTH_TOKEN oder ~/.claude
|
||||
"timeout_factor": 1,
|
||||
},
|
||||
"minimax": {
|
||||
"cli": "opencode",
|
||||
"guide": "minimax/MiniMax-M3",
|
||||
"fast": "minimax/MiniMax-M3",
|
||||
"env_key": "MINIMAX_API_KEY",
|
||||
"timeout_factor": 3, # M3 ist bei großen Dokumenten deutlich langsamer
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,16 +8,12 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from agents import run_agent, kill_process
|
||||
from config import (
|
||||
AGENT_TIMEOUT,
|
||||
CLAUDE_CLI,
|
||||
DEFAULT_PROVIDER,
|
||||
TEMPLATES_DIR,
|
||||
MAX_CONCURRENT_GENERATIONS,
|
||||
MODEL_GUIDE,
|
||||
MODEL_BAUSTEIN_GEN,
|
||||
MODEL_BAUSTEIN_REWORK,
|
||||
MODEL_CHAT,
|
||||
MODEL_PROJECT_INDEX,
|
||||
STORAGE_DIR,
|
||||
)
|
||||
from database import (
|
||||
@@ -32,15 +28,12 @@ from database import (
|
||||
from paths import final_paths, temp_paths, project_dir, project_cache_path
|
||||
|
||||
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
|
||||
_active_processes: dict[str, asyncio.subprocess.Process] = {}
|
||||
_cancelled: set[str] = set()
|
||||
|
||||
|
||||
async def cancel_guide(guide_id: str) -> bool:
|
||||
_cancelled.add(guide_id)
|
||||
process = _active_processes.get(guide_id)
|
||||
if process and process.returncode is None:
|
||||
process.kill()
|
||||
kill_process(guide_id)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen", updated_at=now)
|
||||
return True
|
||||
@@ -51,38 +44,6 @@ async def _set_progress(guide_id: str, progress: str) -> None:
|
||||
await update_guide(guide_id, progress=progress, updated_at=now)
|
||||
|
||||
|
||||
async def _run_claude(guide_id: str, prompt: str, timeout: int, tools: str | None = "Write,Bash,Read,WebSearch,WebFetch", model: str | None = None) -> tuple[int, str, str]:
|
||||
cmd = [CLAUDE_CLI, "-p"]
|
||||
if model:
|
||||
cmd += ["--model", model]
|
||||
if tools:
|
||||
cmd += ["--allowedTools", tools]
|
||||
cmd += ["--dangerously-skip-permissions"]
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_active_processes[guide_id] = process
|
||||
try:
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(input=prompt.encode("utf-8")),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
raise
|
||||
return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
|
||||
finally:
|
||||
_active_processes.pop(guide_id, None)
|
||||
|
||||
|
||||
async def _render_pdf(html_path: Path, pdf_path: Path) -> tuple[bool, str]:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"weasyprint", str(html_path), str(pdf_path),
|
||||
@@ -225,7 +186,7 @@ FAIL
|
||||
"""
|
||||
|
||||
|
||||
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", reindex: bool = False) -> None:
|
||||
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", reindex: bool = False, provider: str = DEFAULT_PROVIDER) -> None:
|
||||
async with _semaphore:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
await update_guide(guide_id, status="generating", progress="Recherche…", updated_at=now)
|
||||
@@ -247,9 +208,9 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
await _set_progress(guide_id, "Lese Projekt…")
|
||||
current_step = "Projekt-Einlesen"
|
||||
index_prompt = _build_project_index_prompt(topic, cache_path, cache_path.exists())
|
||||
returncode, idx_out, idx_err = await _run_claude(
|
||||
returncode, idx_out, idx_err = await run_agent(
|
||||
guide_id, index_prompt, AGENT_TIMEOUT,
|
||||
tools="Read,Bash,Write", model=MODEL_PROJECT_INDEX,
|
||||
provider=provider, role="fast", capabilities="files",
|
||||
)
|
||||
if guide_id in _cancelled:
|
||||
return
|
||||
@@ -264,7 +225,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
# Step 1: Generator-Agent erstellt HTML
|
||||
await _set_progress(guide_id, "Generiere HTML…")
|
||||
gen_prompt = _build_generator_prompt(topic, format_name, html_path, instructions, project_content)
|
||||
returncode, stdout, stderr = await _run_claude(guide_id, gen_prompt, AGENT_TIMEOUT, model=MODEL_GUIDE)
|
||||
returncode, stdout, stderr = await run_agent(guide_id, gen_prompt, AGENT_TIMEOUT, provider=provider, role="guide", capabilities="full")
|
||||
|
||||
if guide_id in _cancelled:
|
||||
return
|
||||
@@ -284,7 +245,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
current_step = "Inhalts-Review"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
content_prompt = _build_content_review_prompt(topic, format_name, html_path, project_content)
|
||||
returncode, review_out, review_err = await _run_claude(guide_id, content_prompt, AGENT_TIMEOUT, model=MODEL_GUIDE)
|
||||
returncode, review_out, review_err = await run_agent(guide_id, content_prompt, AGENT_TIMEOUT, provider=provider, role="guide", capabilities="full")
|
||||
|
||||
if returncode != 0:
|
||||
await _fail(guide_id, _claude_error("Inhalts-Review-Fehler", returncode, review_out, review_err))
|
||||
@@ -300,7 +261,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
current_step = "Inhalts-Korrektur"
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
fix_prompt = _build_fix_prompt(topic, format_name, html_path, feedback)
|
||||
returncode, fix_out, fix_err = await _run_claude(guide_id, fix_prompt, AGENT_TIMEOUT, model=MODEL_GUIDE)
|
||||
returncode, fix_out, fix_err = await run_agent(guide_id, fix_prompt, AGENT_TIMEOUT, provider=provider, role="guide", capabilities="full")
|
||||
|
||||
if returncode != 0:
|
||||
await _fail(guide_id, _claude_error("Fix-Fehler", returncode, fix_out, fix_err))
|
||||
@@ -326,11 +287,10 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
||||
except Exception as e:
|
||||
await _fail(guide_id, str(e)[:2000])
|
||||
finally:
|
||||
_active_processes.pop(guide_id, None)
|
||||
_cancelled.discard(guide_id)
|
||||
|
||||
|
||||
async def rework_guide(guide_id: str, topic: str, format_name: str, instructions: str) -> None:
|
||||
async def rework_guide(guide_id: str, topic: str, format_name: str, instructions: str, provider: str = DEFAULT_PROVIDER) -> None:
|
||||
async with _semaphore:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
await update_guide(guide_id, status="generating", progress="Überarbeite…", updated_at=now)
|
||||
@@ -352,7 +312,7 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
||||
current_timeout = AGENT_TIMEOUT
|
||||
|
||||
rework_prompt = _build_rework_prompt(topic, format_name, tmp_html, instructions)
|
||||
returncode, stdout, stderr = await _run_claude(guide_id, rework_prompt, AGENT_TIMEOUT, model=MODEL_GUIDE)
|
||||
returncode, stdout, stderr = await run_agent(guide_id, rework_prompt, AGENT_TIMEOUT, provider=provider, role="guide", capabilities="full")
|
||||
|
||||
if guide_id in _cancelled:
|
||||
return
|
||||
@@ -384,7 +344,6 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions
|
||||
except Exception as e:
|
||||
await _fail(guide_id, str(e)[:2000])
|
||||
finally:
|
||||
_active_processes.pop(guide_id, None)
|
||||
_cancelled.discard(guide_id)
|
||||
tmp_html.unlink(missing_ok=True)
|
||||
tmp_pdf.unlink(missing_ok=True)
|
||||
@@ -480,7 +439,7 @@ Orientiere dich an der Spezifikation und Referenz. Kein weiterer Text, nur das J
|
||||
"""
|
||||
|
||||
|
||||
async def generate_suggestions(topic: str, html_paths: list[Path]) -> None:
|
||||
async def generate_suggestions(topic: str, html_paths: list[Path], provider: str = DEFAULT_PROVIDER) -> None:
|
||||
_suggestions_generating.add(topic)
|
||||
try:
|
||||
existing = await list_bausteine(topic)
|
||||
@@ -489,8 +448,8 @@ async def generate_suggestions(topic: str, html_paths: list[Path]) -> None:
|
||||
await delete_pending_suggestions(topic)
|
||||
|
||||
prompt = _build_suggestions_prompt(topic, html_paths, existing_titles)
|
||||
tools = "Read" if html_paths else None
|
||||
returncode, stdout, stderr = await _run_claude("suggestions-" + topic, prompt, 1800, tools=tools, model=MODEL_BAUSTEIN_GEN)
|
||||
capabilities = "read" if html_paths else "none"
|
||||
returncode, stdout, stderr = await run_agent("suggestions-" + topic, prompt, 1800, provider=provider, role="fast", capabilities=capabilities)
|
||||
|
||||
if returncode != 0:
|
||||
return
|
||||
@@ -520,10 +479,10 @@ async def generate_suggestions(topic: str, html_paths: list[Path]) -> None:
|
||||
_suggestions_generating.discard(topic)
|
||||
|
||||
|
||||
async def generate_baustein_detail(baustein_id: str, topic: str, title: str, instructions: str = "") -> None:
|
||||
async def generate_baustein_detail(baustein_id: str, topic: str, title: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
|
||||
try:
|
||||
prompt = _build_baustein_detail_prompt(topic, title, instructions)
|
||||
returncode, stdout, stderr = await _run_claude("baustein-" + baustein_id, prompt, 60, tools=None, model=MODEL_BAUSTEIN_GEN)
|
||||
returncode, stdout, stderr = await run_agent("baustein-" + baustein_id, prompt, 180, provider=provider, role="fast", capabilities="none")
|
||||
|
||||
if returncode != 0:
|
||||
return
|
||||
@@ -544,10 +503,10 @@ async def generate_baustein_detail(baustein_id: str, topic: str, title: str, ins
|
||||
pass
|
||||
|
||||
|
||||
async def rework_baustein(baustein_id: str, topic: str, title: str, current: dict, instructions: str) -> None:
|
||||
async def rework_baustein(baustein_id: str, topic: str, title: str, current: dict, instructions: str, provider: str = DEFAULT_PROVIDER) -> None:
|
||||
try:
|
||||
prompt = _build_baustein_rework_prompt(topic, title, current, instructions)
|
||||
returncode, stdout, stderr = await _run_claude("baustein-" + baustein_id, prompt, 60, tools=None, model=MODEL_BAUSTEIN_REWORK)
|
||||
returncode, stdout, stderr = await run_agent("baustein-" + baustein_id, prompt, 180, provider=provider, role="fast", capabilities="none")
|
||||
|
||||
if returncode != 0:
|
||||
return
|
||||
@@ -654,11 +613,11 @@ WICHTIG – Antwortstil:
|
||||
Gib NUR die Antwort aus, kein Präfix wie "Assistent:"."""
|
||||
|
||||
|
||||
async def chat_with_guide(topic: str, format_name: str, section: str, outline: str, messages: list[dict]) -> str:
|
||||
async def chat_with_guide(topic: str, format_name: str, section: str, outline: str, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str:
|
||||
try:
|
||||
prompt = _build_guide_chat_prompt(topic, format_name, section, outline, messages)
|
||||
returncode, stdout, stderr = await _run_claude(
|
||||
"chat-" + str(uuid.uuid4()), prompt, 120, tools=None, model=MODEL_CHAT
|
||||
returncode, stdout, stderr = await run_agent(
|
||||
"chat-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none"
|
||||
)
|
||||
if returncode != 0:
|
||||
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
|
||||
@@ -668,11 +627,11 @@ async def chat_with_guide(topic: str, format_name: str, section: str, outline: s
|
||||
return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut."
|
||||
|
||||
|
||||
async def suggest_topics(problem: str, existing_topics: list[str] | None = None) -> list[dict]:
|
||||
async def suggest_topics(problem: str, existing_topics: list[str] | None = None, provider: str = DEFAULT_PROVIDER) -> list[dict]:
|
||||
try:
|
||||
prompt = _build_topic_suggest_prompt(problem, existing_topics or [])
|
||||
returncode, stdout, stderr = await _run_claude(
|
||||
"topic-suggest-" + str(uuid.uuid4()), prompt, 120, tools=None, model=MODEL_BAUSTEIN_GEN
|
||||
returncode, stdout, stderr = await run_agent(
|
||||
"topic-suggest-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none"
|
||||
)
|
||||
if returncode != 0:
|
||||
return []
|
||||
@@ -692,11 +651,11 @@ async def suggest_topics(problem: str, existing_topics: list[str] | None = None)
|
||||
return []
|
||||
|
||||
|
||||
async def sort_bausteine(topic: str, bausteine: list[dict], instructions: str = "") -> None:
|
||||
async def sort_bausteine(topic: str, bausteine: list[dict], instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
|
||||
_sorting.add(topic)
|
||||
try:
|
||||
prompt = _build_sort_prompt(topic, bausteine, instructions)
|
||||
returncode, stdout, stderr = await _run_claude("sort-" + topic, prompt, 300, tools=None, model=MODEL_BAUSTEIN_GEN)
|
||||
returncode, stdout, stderr = await run_agent("sort-" + topic, prompt, 600, provider=provider, role="fast", capabilities="none")
|
||||
if returncode != 0:
|
||||
return
|
||||
ids = _parse_json(stdout)
|
||||
|
||||
@@ -9,12 +9,15 @@ FormatType = Literal[
|
||||
"EndGuide",
|
||||
]
|
||||
|
||||
ProviderType = Literal["claude", "minimax"]
|
||||
|
||||
|
||||
class GuideCreateRequest(BaseModel):
|
||||
topic: str = Field(min_length=1, max_length=100)
|
||||
format: FormatType
|
||||
instructions: str = Field(default="", max_length=2000)
|
||||
reindex: bool = False
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class ProjectResponse(BaseModel):
|
||||
@@ -22,8 +25,14 @@ class ProjectResponse(BaseModel):
|
||||
cached: bool
|
||||
|
||||
|
||||
class ProviderInfo(BaseModel):
|
||||
id: str
|
||||
available: bool
|
||||
|
||||
|
||||
class GuideReworkRequest(BaseModel):
|
||||
instructions: str = Field(min_length=1, max_length=2000)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class GuideResponse(BaseModel):
|
||||
@@ -41,10 +50,12 @@ class BausteinCreateRequest(BaseModel):
|
||||
topic: str = Field(min_length=1, max_length=100)
|
||||
title: str = Field(min_length=1, max_length=200)
|
||||
instructions: str = Field(default="", max_length=2000)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class BausteinReworkRequest(BaseModel):
|
||||
instructions: str = Field(min_length=1, max_length=2000)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class BausteinResponse(BaseModel):
|
||||
@@ -61,6 +72,7 @@ class BausteinResponse(BaseModel):
|
||||
|
||||
class BausteinSortRequest(BaseModel):
|
||||
instructions: str = Field(default="", max_length=2000)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class SuggestionResponse(BaseModel):
|
||||
@@ -76,6 +88,7 @@ class SuggestionResponse(BaseModel):
|
||||
|
||||
class TopicSuggestRequest(BaseModel):
|
||||
problem: str = Field(min_length=1, max_length=2000)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class TopicSuggestion(BaseModel):
|
||||
@@ -92,6 +105,7 @@ class GuideChatRequest(BaseModel):
|
||||
section: str = Field(default="", max_length=20000)
|
||||
outline: str = Field(default="", max_length=8000)
|
||||
messages: list[ChatMessage] = Field(min_length=1)
|
||||
provider: ProviderType = "claude"
|
||||
|
||||
|
||||
class GuideChatResponse(BaseModel):
|
||||
|
||||
@@ -6,7 +6,8 @@ from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from config import FORMAT_META, PROJECTS_DIR
|
||||
from agents import provider_available
|
||||
from config import FORMAT_META, PROJECTS_DIR, PROVIDERS
|
||||
from database import (
|
||||
create_guide, delete_guide, get_guide, list_guides,
|
||||
create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein,
|
||||
@@ -19,7 +20,7 @@ from models import (
|
||||
BausteinCreateRequest, BausteinReworkRequest, BausteinSortRequest, BausteinResponse, SuggestionResponse,
|
||||
TopicSuggestRequest, TopicSuggestion,
|
||||
GuideChatRequest, GuideChatResponse,
|
||||
ProgressUpdate, ProgressResponse, ProjectResponse,
|
||||
ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo,
|
||||
)
|
||||
from paths import final_paths, project_dir, project_cache_path
|
||||
|
||||
@@ -31,6 +32,11 @@ async def get_formats():
|
||||
return FORMAT_META
|
||||
|
||||
|
||||
@router.get("/providers", response_model=list[ProviderInfo])
|
||||
async def get_providers():
|
||||
return [{"id": pid, "available": provider_available(pid)} for pid in PROVIDERS]
|
||||
|
||||
|
||||
def _safe_project_name(name: str) -> str:
|
||||
if not name or "/" in name or "\\" in name or ".." in name or "\x00" in name:
|
||||
raise HTTPException(400, "Ungültiger Projektname")
|
||||
@@ -63,7 +69,7 @@ async def remove_project(name: str):
|
||||
async def topic_suggestions(req: TopicSuggestRequest):
|
||||
guides = await list_guides()
|
||||
existing_topics = sorted({g["topic"] for g in guides})
|
||||
return await suggest_topics(req.problem.strip(), existing_topics)
|
||||
return await suggest_topics(req.problem.strip(), existing_topics, provider=req.provider)
|
||||
|
||||
|
||||
@router.post("/guides", response_model=GuideResponse)
|
||||
@@ -80,7 +86,7 @@ async def create(req: GuideCreateRequest):
|
||||
"updated_at": now,
|
||||
}
|
||||
await create_guide(guide)
|
||||
asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.reindex))
|
||||
asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.reindex, req.provider))
|
||||
return guide
|
||||
|
||||
|
||||
@@ -117,7 +123,7 @@ async def rework(guide_id: str, req: GuideReworkRequest):
|
||||
raise HTTPException(404, "Guide nicht gefunden")
|
||||
if guide["status"] != "done":
|
||||
raise HTTPException(400, "Guide muss fertig sein")
|
||||
asyncio.create_task(rework_guide(guide_id, guide["topic"], guide["format"], req.instructions.strip()))
|
||||
asyncio.create_task(rework_guide(guide_id, guide["topic"], guide["format"], req.instructions.strip(), req.provider))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -129,6 +135,7 @@ async def guide_chat(guide_id: str, req: GuideChatRequest):
|
||||
reply = await chat_with_guide(
|
||||
guide["topic"], guide["format"], req.section, req.outline,
|
||||
[m.model_dump() for m in req.messages],
|
||||
provider=req.provider,
|
||||
)
|
||||
return {"reply": reply}
|
||||
|
||||
@@ -205,7 +212,7 @@ async def add_baustein(req: BausteinCreateRequest):
|
||||
"updated_at": now,
|
||||
}
|
||||
await db_create_baustein(baustein)
|
||||
asyncio.create_task(generate_baustein_detail(baustein["id"], baustein["topic"], baustein["title"], req.instructions.strip()))
|
||||
asyncio.create_task(generate_baustein_detail(baustein["id"], baustein["topic"], baustein["title"], req.instructions.strip(), req.provider))
|
||||
return baustein
|
||||
|
||||
|
||||
@@ -233,7 +240,7 @@ async def rework_baustein_route(baustein_id: str, req: BausteinReworkRequest):
|
||||
"purpose": b.get("purpose", ""),
|
||||
"examples": examples,
|
||||
}
|
||||
asyncio.create_task(rework_baustein(baustein_id, b["topic"], b["title"], current, req.instructions.strip()))
|
||||
asyncio.create_task(rework_baustein(baustein_id, b["topic"], b["title"], current, req.instructions.strip(), req.provider))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -244,7 +251,7 @@ async def sort_bausteine_route(topic: str, req: BausteinSortRequest):
|
||||
bausteine = await list_bausteine(topic)
|
||||
if not bausteine:
|
||||
return {"ok": True}
|
||||
asyncio.create_task(sort_bausteine(topic, bausteine, req.instructions.strip()))
|
||||
asyncio.create_task(sort_bausteine(topic, bausteine, req.instructions.strip(), req.provider))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -261,7 +268,9 @@ async def get_suggestions(topic: str):
|
||||
|
||||
|
||||
@router.post("/bausteine/suggestions/generate")
|
||||
async def trigger_suggestions(topic: str):
|
||||
async def trigger_suggestions(topic: str, provider: str = "claude"):
|
||||
if provider not in PROVIDERS:
|
||||
raise HTTPException(400, "Unbekannter Provider")
|
||||
if is_suggestions_generating(topic):
|
||||
return {"ok": True, "status": "already_generating"}
|
||||
guides = await list_guides()
|
||||
@@ -271,7 +280,7 @@ async def trigger_suggestions(topic: str):
|
||||
html_path, _ = final_paths(g["topic"], g["format"])
|
||||
if html_path.exists():
|
||||
html_paths.append(html_path)
|
||||
asyncio.create_task(generate_suggestions(topic, html_paths))
|
||||
asyncio.create_task(generate_suggestions(topic, html_paths, provider))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user