update
This commit is contained in:
@@ -29,7 +29,7 @@ import httpx
|
||||
|
||||
from config import (MAX_CONCURRENT_AGENTS, MAX_CONCURRENT_AGENTS_PER_TOPIC,
|
||||
MAX_CONCURRENT_API_AGENTS, OPENCODE_CONFIG, PROVIDERS,
|
||||
RAM_MIN_FREE_PCT, resolve_role)
|
||||
RAM_MIN_FREE_PCT, fake_agents_aktiv, resolve_role)
|
||||
|
||||
log = logging.getLogger("planer.agents")
|
||||
|
||||
@@ -270,7 +270,7 @@ async def run_agent(
|
||||
"""Ein LLM-Call der Rolle `role`. → (rc, stdout, stderr). Wirft AgentInfraError,
|
||||
wenn die Infrastruktur nicht nutzbar ist (Rolle unkonfiguriert, Provider down,
|
||||
leere Antwort ohne Session) — der Lauf soll dann pausieren, nicht raten."""
|
||||
if os.getenv("PLANER_FAKE_AGENTS"): # Sekunden-Smoke: deterministisch statt LLM
|
||||
if fake_agents_aktiv(): # Sekunden-Smoke: deterministisch statt LLM
|
||||
import fake_agents
|
||||
start = time.monotonic()
|
||||
res = await fake_agents.respond(agent_key, prompt, capabilities)
|
||||
@@ -514,7 +514,10 @@ async def _run_text_api(agent_key: str, prompt: str, timeout: int, model: str,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
**_api_model_opts(model),
|
||||
}
|
||||
headers = {"x-api-key": os.environ.get("MINIMAX_API_KEY", ""), "anthropic-version": _API_VERSION}
|
||||
api_key = os.environ.get("MINIMAX_API_KEY")
|
||||
if not api_key:
|
||||
raise AgentInfraError("MINIMAX_API_KEY fehlt — in .env setzen")
|
||||
headers = {"x-api-key": api_key, "anthropic-version": _API_VERSION}
|
||||
track_key = agent_key
|
||||
n = 2
|
||||
while track_key in _active_api:
|
||||
|
||||
@@ -61,6 +61,12 @@ def resolve_role(role: str) -> tuple[str, str]:
|
||||
return provider, modell
|
||||
|
||||
|
||||
def fake_agents_aktiv() -> bool:
|
||||
"""PLANER_FAKE_AGENTS: leer oder "0" = aus. Ein truthy "0" würde sonst still den
|
||||
Fake-Modus (und damit den Rollen-Vorflug-Check) aktivieren."""
|
||||
return os.getenv("PLANER_FAKE_AGENTS", "").strip() not in ("", "0")
|
||||
|
||||
|
||||
# --- Engine-Konstanten (Kanban + Agenten) -----------------------------------------
|
||||
KANBAN_BATCH = int(os.getenv("KANBAN_BATCH", "4")) # Karten je Paket
|
||||
MAX_CARD_RETRIES = int(os.getenv("MAX_CARD_RETRIES", "3")) # dann dead-letter
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Projektliste: konfigurierte Pfade aus projekte.txt (eine Zeile = absoluter Pfad,
|
||||
#-Kommentare). Von Hand editierbar — UI-Verwaltung kommt später (YAGNI)."""
|
||||
#-Kommentare). Von Hand editierbar; hinzufuegen/entfernen bedienen die UI."""
|
||||
from pathlib import Path
|
||||
|
||||
import config
|
||||
@@ -34,3 +34,41 @@ def finde(name: str) -> Path | None:
|
||||
if p["name"] == name and "fehler" not in p:
|
||||
return Path(p["pfad"])
|
||||
return None
|
||||
|
||||
|
||||
def hinzufuegen(pfad: str) -> None:
|
||||
"""Hängt einen Pfad an projekte.txt an. Wirft ValueError bei ungültigem
|
||||
Verzeichnis, Duplikat oder Basename-Kollision."""
|
||||
p = Path(pfad).expanduser()
|
||||
if not p.is_absolute():
|
||||
raise ValueError("Pfad muss absolut sein")
|
||||
if not p.is_dir():
|
||||
raise ValueError(f"{p} ist kein Verzeichnis")
|
||||
for e in lade():
|
||||
if e["pfad"] == str(p):
|
||||
raise ValueError(f"{p} ist schon eingetragen")
|
||||
if e["name"] == p.name:
|
||||
raise ValueError(f"Namenskollision mit {e['pfad']}")
|
||||
text = ""
|
||||
if config.PROJEKTE_DATEI.is_file():
|
||||
text = config.PROJEKTE_DATEI.read_text(encoding="utf-8")
|
||||
if text and not text.endswith("\n"):
|
||||
text += "\n"
|
||||
config.PROJEKTE_DATEI.write_text(text + str(p) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def entfernen(name: str) -> None:
|
||||
"""Streicht den Eintrag aus projekte.txt — nur die Zeile; .planer/ im Repo
|
||||
bleibt unberührt. Wirft ValueError, wenn der Name nicht eingetragen ist."""
|
||||
if not config.PROJEKTE_DATEI.is_file():
|
||||
raise ValueError(f"'{name}' nicht in projekte.txt")
|
||||
rest, getroffen = [], False
|
||||
for zeile in config.PROJEKTE_DATEI.read_text(encoding="utf-8").splitlines():
|
||||
s = zeile.strip()
|
||||
if s and not s.startswith("#") and Path(s).expanduser().name == name:
|
||||
getroffen = True
|
||||
continue
|
||||
rest.append(zeile)
|
||||
if not getroffen:
|
||||
raise ValueError(f"'{name}' nicht in projekte.txt")
|
||||
config.PROJEKTE_DATEI.write_text("\n".join(rest) + ("\n" if rest else ""), encoding="utf-8")
|
||||
|
||||
@@ -24,6 +24,47 @@ async def health():
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/fs")
|
||||
async def fs(pfad: str = ""):
|
||||
"""Ordner-Browser für die Projekt-Auswahl: nur Verzeichnisse, keine versteckten."""
|
||||
basis = Path(pfad).expanduser() if pfad else Path.home()
|
||||
if not basis.is_dir():
|
||||
raise HTTPException(404, f"{basis} ist kein Verzeichnis")
|
||||
basis = basis.resolve()
|
||||
try:
|
||||
kinder = sorted(basis.iterdir(), key=lambda p: p.name.lower())
|
||||
except PermissionError:
|
||||
raise HTTPException(403, f"kein Zugriff auf {basis}")
|
||||
ordner = [{"name": k.name, "pfad": str(k), "hat_planer": (k / ".planer").is_dir()}
|
||||
for k in kinder if k.is_dir() and not k.name.startswith(".")]
|
||||
eltern = str(basis.parent) if basis.parent != basis else None
|
||||
return {"pfad": str(basis), "eltern": eltern, "ordner": ordner}
|
||||
|
||||
|
||||
@router.post("/projekte")
|
||||
async def projekt_hinzufuegen(body: dict):
|
||||
pfad = (body.get("pfad") or "").strip()
|
||||
if not pfad:
|
||||
raise HTTPException(400, "pfad fehlt")
|
||||
try:
|
||||
projekte.hinzufuegen(pfad)
|
||||
except ValueError as e:
|
||||
raise HTTPException(409, str(e))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/projekte/{name}")
|
||||
async def projekt_entfernen(name: str):
|
||||
lauf = scan_dienst.status(name)
|
||||
if lauf and lauf["laeuft"]:
|
||||
raise HTTPException(409, "Scan läuft — erst abwarten oder abbrechen")
|
||||
try:
|
||||
projekte.entfernen(name)
|
||||
except ValueError as e:
|
||||
raise HTTPException(404, str(e))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/projekte")
|
||||
async def liste():
|
||||
out = []
|
||||
|
||||
@@ -34,7 +34,7 @@ def vorflug(wurzel: Path) -> str:
|
||||
raise VorflugFehler(f"{wurzel} ist kein git-Repo — Anker brauchen Commit-Hashes (git init && commit)")
|
||||
if not repo.working_tree_sauber(wurzel):
|
||||
raise VorflugFehler(f"{wurzel} hat uncommittete Änderungen — erst committen, dann scannen")
|
||||
if not os.getenv("PLANER_FAKE_AGENTS"):
|
||||
if not config.fake_agents_aktiv():
|
||||
for rolle in config.ROLLEN:
|
||||
try:
|
||||
provider, _ = config.resolve_role(rolle)
|
||||
|
||||
@@ -85,3 +85,34 @@ async def test_scan_409_bei_dirty_tree(client):
|
||||
r = await c.post("/api/projekte/mini/scan", json={"art": "scan"})
|
||||
assert r.status_code == 409
|
||||
assert "uncommittete" in r.json()["detail"]
|
||||
|
||||
|
||||
async def test_fs_browse(client, tmp_path):
|
||||
c, _, _ = client
|
||||
(tmp_path / "unter" / ".planer").mkdir(parents=True)
|
||||
(tmp_path / ".versteckt").mkdir()
|
||||
r = await c.get("/api/fs", params={"pfad": str(tmp_path)})
|
||||
daten = r.json()
|
||||
assert daten["pfad"] == str(tmp_path.resolve())
|
||||
assert daten["eltern"] == str(tmp_path.resolve().parent)
|
||||
ordner = {o["name"]: o for o in daten["ordner"]}
|
||||
assert ordner["unter"]["hat_planer"] is True
|
||||
assert ".versteckt" not in ordner
|
||||
assert (await c.get("/api/fs", params={"pfad": str(tmp_path / "fehlt")})).status_code == 404
|
||||
|
||||
|
||||
async def test_projekt_hinzufuegen_und_entfernen(client, tmp_path):
|
||||
c, _, _ = client
|
||||
neu = tmp_path / "neues-repo"
|
||||
(neu / ".planer").mkdir(parents=True)
|
||||
|
||||
assert (await c.post("/api/projekte", json={"pfad": str(neu)})).status_code == 200
|
||||
namen = [p["name"] for p in (await c.get("/api/projekte")).json()]
|
||||
assert "neues-repo" in namen and "mini" in namen
|
||||
assert (await c.post("/api/projekte", json={"pfad": str(neu)})).status_code == 409
|
||||
|
||||
assert (await c.delete("/api/projekte/neues-repo")).status_code == 200
|
||||
namen = [p["name"] for p in (await c.get("/api/projekte")).json()]
|
||||
assert "neues-repo" not in namen and "mini" in namen
|
||||
assert (neu / ".planer").is_dir() # Artefakte bleiben erhalten
|
||||
assert (await c.delete("/api/projekte/neues-repo")).status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user