Files
clownfischserver/bot/aider_wrapper.py
T
mehlzoerwerandClaude Haiku 4.5 eff7d0d98b v0.5.0 – Clownfischserver HomeIP Auto-Update: myfritz Automatik für SSH (5-Min Cron)
Features:
- install.sh: Frage in Step 4 ob myfritz-Adresse für Auto-SSH-Freigabe genutzt werden soll
- clownfischserver-homeip-updater.sh: DNS-Auflösung + UFW-Update (alle 5 Min via Cron)
- .env-Variable: CLOWNFISCHSERVER_HOME_IP_DOMAIN für myfritz-Adresse
- State-File tracking zur Vermeidung unnötiger UFW-Updates
- Logging: /var/log/clownfischserver-homeip.log
- Dokumentation: scripts/README-HOMEIP.md + CLOWNFISCHSERVER.md update

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-01 23:28:36 +02:00

129 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
🐠 Clownfischserver
Author: Mehlzoerwer-Claude (https://github.com/mehlzoerwer-claude)
License: GPL-3.0 – Keep it open. Always.
Repo: https://github.com/mehlzoerwer-claude/clownfischserver
"""
import asyncio
import logging
import os
import tempfile
from pathlib import Path
logger = logging.getLogger(__name__)
INSTALL_DIR = os.getenv("INSTALL_DIR", "/opt/clownfischserver")
AIDER_WORKDIR = os.getenv("AIDER_WORKDIR", "/opt/clownfisch-workspace")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:2b")
TIMEOUT = 600 # 10 minutes for aider tasks on CPU
class AiderWrapper:
def __init__(self):
self.workdir = Path(AIDER_WORKDIR)
self.workdir.mkdir(parents=True, exist_ok=True)
self.venv_python = Path(INSTALL_DIR) / "aider-venv" / "bin" / "python3"
self.venv_aider = Path(INSTALL_DIR) / "aider-venv" / "bin" / "aider"
logger.info(f"AiderWrapper init: workdir={self.workdir}")
async def run(self, task: str, target_file: str = None) -> str:
"""
Run aider in script mode for a coding task.
Returns the output/result as string.
"""
logger.info(f"Aider task: {task[:100]}")
# Determine working directory and files
workdir = self.workdir
# Build aider command
cmd = self._build_aider_command(task, target_file)
try:
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(workdir),
env={
**os.environ,
"OLLAMA_API_BASE": OLLAMA_BASE_URL,
"HOME": f"/home/{os.getenv('MGMT_USER', 'clownfish')}",
}
)
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=TIMEOUT
)
except asyncio.TimeoutError:
process.kill()
return f"⏱️ Aider Timeout nach {TIMEOUT}s. Task zu komplex oder Modell überlastet."
stdout_str = stdout.decode("utf-8", errors="replace").strip()
stderr_str = stderr.decode("utf-8", errors="replace").strip()
# Build response
result_parts = []
if stdout_str:
result_parts.append(stdout_str)
if stderr_str and process.returncode != 0:
result_parts.append(f"[STDERR]\n{stderr_str}")
if process.returncode == 0:
result_parts.insert(0, f"✅ Aider Task abgeschlossen\nArbeitsverzeichnis: {workdir}\n")
else:
result_parts.insert(0, f"⚠️ Aider beendet mit Code {process.returncode}\n")
return "\n".join(result_parts) or "✓ Task abgeschlossen (keine Ausgabe)"
except FileNotFoundError:
return (
"❌ Aider nicht gefunden. Bitte prüfen:\n"
f" ls {self.venv_aider}\n"
" Ggf. neu installieren mit: pip install aider-chat"
)
except Exception as e:
logger.error(f"Aider execution error: {e}", exc_info=True)
return f"❌ Aider Fehler: {e}"
def _build_aider_command(self, task: str, target_file: str = None) -> list:
"""Build the aider command with appropriate flags"""
cmd = [
str(self.venv_aider),
"--model", f"ollama/{OLLAMA_MODEL}",
"--no-git", # Don't require git
"--yes", # Auto-confirm all changes
"--no-pretty", # Plain output for telegram
"--no-show-model-warnings", # Skip env var warnings
"--no-analytics", # No analytics prompts
"--edit-format", "diff", # Better format for small models
"--message", task, # The task as a message
]
if target_file:
cmd.append(target_file)
return cmd
async def list_workspace_files(self) -> str:
"""List files in the aider workspace"""
try:
files = list(self.workdir.rglob("*"))
if not files:
return f"📁 Workspace {self.workdir} ist leer."
file_list = "\n".join([
f" {'📁' if f.is_dir() else '📄'} {f.relative_to(self.workdir)}"
for f in sorted(files)[:50]
])
return f"📁 Workspace: {self.workdir}\n\n{file_list}"
except Exception as e:
return f"❌ Fehler beim Lesen des Workspace: {e}"