added debugging, switch to json
This commit is contained in:
parent
fcffccb718
commit
56f1f4e8fb
3 changed files with 509 additions and 523 deletions
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766473571,
|
"lastModified": 1772047000,
|
||||||
"narHash": "sha256-5G1NDO2PulBx1RoaA6U1YoUDX0qZslpPxv+n5GX6Qto=",
|
"narHash": "sha256-7DaQVv4R97cii/Qdfy4tmDZMB2xxtyIvNGSwXBBhSmo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "76701a179d3a98b07653e2b0409847499b2a07d3",
|
"rev": "1267bb4920d0fc06ea916734c11b0bf004bbe17e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
overlay = final: prev: {
|
overlay = final: prev: {
|
||||||
meinantrag = with final; python3Packages.buildPythonApplication rec {
|
meinantrag = with final; python3Packages.buildPythonApplication rec {
|
||||||
pname = "meinantrag";
|
pname = "meinantrag";
|
||||||
version = "0.0.2";
|
version = "0.0.3";
|
||||||
format = "other";
|
format = "other";
|
||||||
|
|
||||||
src = self;
|
src = self;
|
||||||
|
|
|
||||||
424
meinantrag.py
424
meinantrag.py
|
|
@ -14,15 +14,27 @@ import google.generativeai as genai
|
||||||
import re
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from docx.shared import Pt, Inches
|
from docx.shared import Pt, Inches
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
|
||||||
DOCX_AVAILABLE = True
|
DOCX_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
DOCX_AVAILABLE = False
|
DOCX_AVAILABLE = False
|
||||||
|
|
||||||
SITE_BASE_URL = os.environ.get('MEINANTRAG_BASE_URL', 'http://localhost:8000')
|
# Setup logging
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=os.environ.get('LOG_LEVEL', 'INFO').upper(),
|
||||||
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SITE_BASE_URL = os.environ.get("MEINANTRAG_BASE_URL", "http://localhost:8000")
|
||||||
|
|
||||||
|
|
||||||
class BaseTemplateResource:
|
class BaseTemplateResource:
|
||||||
"""Base class for resources that need template rendering"""
|
"""Base class for resources that need template rendering"""
|
||||||
|
|
@ -30,7 +42,7 @@ class BaseTemplateResource:
|
||||||
def _get_template_dir(self):
|
def _get_template_dir(self):
|
||||||
"""Get the template directory path, handling both development and installed environments"""
|
"""Get the template directory path, handling both development and installed environments"""
|
||||||
# Allow overriding via environment variable (for packaged deployments)
|
# Allow overriding via environment variable (for packaged deployments)
|
||||||
env_dir = os.environ.get('MEINANTRAG_TEMPLATES_DIR')
|
env_dir = os.environ.get("MEINANTRAG_TEMPLATES_DIR")
|
||||||
if env_dir and os.path.exists(env_dir):
|
if env_dir and os.path.exists(env_dir):
|
||||||
return env_dir
|
return env_dir
|
||||||
|
|
||||||
|
|
@ -38,36 +50,43 @@ class BaseTemplateResource:
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
# Try development templates first
|
# Try development templates first
|
||||||
dev_template_dir = os.path.join(script_dir, 'templates')
|
dev_template_dir = os.path.join(script_dir, "templates")
|
||||||
if os.path.exists(dev_template_dir):
|
if os.path.exists(dev_template_dir):
|
||||||
return dev_template_dir
|
return dev_template_dir
|
||||||
|
|
||||||
# Try to find templates relative to the executable
|
# Try to find templates relative to the executable
|
||||||
try:
|
try:
|
||||||
# If we're running from a Nix store, look for templates in share/meinantrag
|
# If we're running from a Nix store, look for templates in share/meinantrag
|
||||||
if '/nix/store/' in script_dir:
|
if "/nix/store/" in script_dir:
|
||||||
# Go up from bin to share/meinantrag/templates
|
# Go up from bin to share/meinantrag/templates
|
||||||
share_dir = os.path.join(script_dir, '..', 'share', 'meinantrag', 'templates')
|
share_dir = os.path.join(
|
||||||
|
script_dir, "..", "share", "meinantrag", "templates"
|
||||||
|
)
|
||||||
if os.path.exists(share_dir):
|
if os.path.exists(share_dir):
|
||||||
return share_dir
|
return share_dir
|
||||||
|
|
||||||
# Alternative: look for templates in the same store path
|
# Alternative: look for templates in the same store path
|
||||||
store_root = script_dir.split('/nix/store/')[1].split('/')[0]
|
store_root = script_dir.split("/nix/store/")[1].split("/")[0]
|
||||||
store_path = f"/nix/store/{store_root}"
|
store_path = f"/nix/store/{store_root}"
|
||||||
alt_share_dir = os.path.join(store_path, 'share', 'meinantrag', 'templates')
|
alt_share_dir = os.path.join(
|
||||||
|
store_path, "share", "meinantrag", "templates"
|
||||||
|
)
|
||||||
if os.path.exists(alt_share_dir):
|
if os.path.exists(alt_share_dir):
|
||||||
return alt_share_dir
|
return alt_share_dir
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Last resort: try to find any templates directory
|
# Last resort: try to find any templates directory
|
||||||
for root, dirs, files in os.walk('/nix/store'):
|
for root, dirs, files in os.walk("/nix/store"):
|
||||||
if 'templates' in dirs and 'index.html' in os.listdir(os.path.join(root, 'templates')):
|
if "templates" in dirs and "index.html" in os.listdir(
|
||||||
return os.path.join(root, 'templates')
|
os.path.join(root, "templates")
|
||||||
|
):
|
||||||
|
return os.path.join(root, "templates")
|
||||||
|
|
||||||
# Fallback to current directory
|
# Fallback to current directory
|
||||||
return dev_template_dir
|
return dev_template_dir
|
||||||
|
|
||||||
|
|
||||||
class MeinAntragApp(BaseTemplateResource):
|
class MeinAntragApp(BaseTemplateResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Setup Jinja2 template environment
|
# Setup Jinja2 template environment
|
||||||
|
|
@ -77,14 +96,15 @@ class MeinAntragApp(BaseTemplateResource):
|
||||||
|
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
"""Serve the main page"""
|
"""Serve the main page"""
|
||||||
template = self.jinja_env.get_template('index.html')
|
template = self.jinja_env.get_template("index.html")
|
||||||
resp.content_type = 'text/html; charset=utf-8'
|
resp.content_type = "text/html; charset=utf-8"
|
||||||
resp.text = template.render(
|
resp.text = template.render(
|
||||||
meta_title='MeinAntrag – Anträge an die Karlsruher Stadtverwaltung',
|
meta_title="MeinAntrag – Anträge an die Karlsruher Stadtverwaltung",
|
||||||
meta_description='Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!',
|
meta_description="Erstelle einfach Vorlagen für Anfragen oder Anträge an die Karlsruher Stadtverwaltung zu deinem persönlichen Thema und schicke diese direkt an eine Stadtratsfraktion!",
|
||||||
canonical_url=f"{SITE_BASE_URL}/"
|
canonical_url=f"{SITE_BASE_URL}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ImpressumResource(BaseTemplateResource):
|
class ImpressumResource(BaseTemplateResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
template_dir = self._get_template_dir()
|
template_dir = self._get_template_dir()
|
||||||
|
|
@ -92,15 +112,16 @@ class ImpressumResource(BaseTemplateResource):
|
||||||
|
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
"""Serve the Impressum page"""
|
"""Serve the Impressum page"""
|
||||||
template = self.jinja_env.get_template('impressum.html')
|
template = self.jinja_env.get_template("impressum.html")
|
||||||
resp.content_type = 'text/html; charset=utf-8'
|
resp.content_type = "text/html; charset=utf-8"
|
||||||
resp.text = template.render(
|
resp.text = template.render(
|
||||||
meta_title='Impressum – MeinAntrag',
|
meta_title="Impressum – MeinAntrag",
|
||||||
meta_description='Impressum für MeinAntrag.',
|
meta_description="Impressum für MeinAntrag.",
|
||||||
canonical_url=f"{SITE_BASE_URL}/impressum",
|
canonical_url=f"{SITE_BASE_URL}/impressum",
|
||||||
noindex=True
|
noindex=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatenschutzResource(BaseTemplateResource):
|
class DatenschutzResource(BaseTemplateResource):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
template_dir = self._get_template_dir()
|
template_dir = self._get_template_dir()
|
||||||
|
|
@ -108,22 +129,23 @@ class DatenschutzResource(BaseTemplateResource):
|
||||||
|
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
"""Serve the Datenschutz page"""
|
"""Serve the Datenschutz page"""
|
||||||
template = self.jinja_env.get_template('datenschutz.html')
|
template = self.jinja_env.get_template("datenschutz.html")
|
||||||
resp.content_type = 'text/html; charset=utf-8'
|
resp.content_type = "text/html; charset=utf-8"
|
||||||
resp.text = template.render(
|
resp.text = template.render(
|
||||||
meta_title='Datenschutz – MeinAntrag',
|
meta_title="Datenschutz – MeinAntrag",
|
||||||
meta_description='Datenschutzerklärung für MeinAntrag. Keine Cookies, es werden nur Anfragen an die FragDenStaat-API gestellt.',
|
meta_description="Datenschutzerklärung für MeinAntrag. Keine Cookies, es werden nur Anfragen an die FragDenStaat-API gestellt.",
|
||||||
canonical_url=f"{SITE_BASE_URL}/datenschutz",
|
canonical_url=f"{SITE_BASE_URL}/datenschutz",
|
||||||
noindex=True
|
noindex=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GenerateAntragResource:
|
class GenerateAntragResource:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Initialize Gemini API
|
# Initialize Gemini API
|
||||||
api_key = os.environ.get('GOOGLE_GEMINI_API_KEY')
|
api_key = os.environ.get("GOOGLE_GEMINI_API_KEY")
|
||||||
if api_key:
|
if api_key:
|
||||||
genai.configure(api_key=api_key)
|
genai.configure(api_key=api_key)
|
||||||
self.model = genai.GenerativeModel('gemini-3-pro-preview')
|
self.model = genai.GenerativeModel("gemini-2.5-pro")
|
||||||
else:
|
else:
|
||||||
self.model = None
|
self.model = None
|
||||||
|
|
||||||
|
|
@ -133,122 +155,65 @@ class GenerateAntragResource:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
# Remove bold/italic markdown: **text** or *text* or __text__ or _text_
|
# Remove bold/italic markdown: **text** or *text* or __text__ or _text_
|
||||||
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
|
||||||
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
text = re.sub(r"\*(.+?)\*", r"\1", text)
|
||||||
text = re.sub(r'__(.+?)__', r'\1', text)
|
text = re.sub(r"__(.+?)__", r"\1", text)
|
||||||
text = re.sub(r'_(.+?)_', r'\1', text)
|
text = re.sub(r"_(.+?)_", r"\1", text)
|
||||||
|
|
||||||
# Remove heading markdown: /Heading or # Heading
|
# Remove heading markdown: /Heading or # Heading
|
||||||
text = re.sub(r'^/\s*', '', text, flags=re.MULTILINE)
|
text = re.sub(r"^/\s*", "", text, flags=re.MULTILINE)
|
||||||
text = re.sub(r'^#+\s*', '', text, flags=re.MULTILINE)
|
text = re.sub(r"^#+\s*", "", text, flags=re.MULTILINE)
|
||||||
|
|
||||||
# Remove other markdown elements
|
# Remove other markdown elements
|
||||||
text = re.sub(r'`(.+?)`', r'\1', text) # Code
|
text = re.sub(r"`(.+?)`", r"\1", text) # Code
|
||||||
text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', text) # Links
|
text = re.sub(r"\[(.+?)\]\(.+?\)", r"\1", text) # Links
|
||||||
|
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
def _parse_gemini_response(self, text):
|
|
||||||
"""Parse the response from Gemini into title, demand, and justification"""
|
|
||||||
# Remove markdown formatting first
|
|
||||||
text = self._remove_markdown(text)
|
|
||||||
|
|
||||||
# Split by "Begründung/Sachverhalt" or similar patterns
|
|
||||||
parts = re.split(r'(begründung|sachverhalt|begründung/sachverhalt)', text, maxsplit=1, flags=re.IGNORECASE)
|
|
||||||
|
|
||||||
if len(parts) >= 3:
|
|
||||||
# We have a split at "Begründung/Sachverhalt"
|
|
||||||
before_justification = parts[0].strip()
|
|
||||||
justification = parts[2].strip() if len(parts) > 2 else ""
|
|
||||||
|
|
||||||
# Remove "Begründung/Sachverhalt" or "Sachverhalt" from the beginning of justification
|
|
||||||
justification = re.sub(r'^(begründung\s*/?\s*sachverhalt|sachverhalt|begründung)\s*:?\s*\n?', '', justification, flags=re.IGNORECASE).strip()
|
|
||||||
else:
|
|
||||||
# Try to split by paragraphs
|
|
||||||
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
|
|
||||||
if len(paragraphs) >= 3:
|
|
||||||
before_justification = '\n\n'.join(paragraphs[:-1])
|
|
||||||
justification = paragraphs[-1]
|
|
||||||
# Remove "Begründung/Sachverhalt" or "Sachverhalt" from the beginning
|
|
||||||
justification = re.sub(r'^(begründung\s*/?\s*sachverhalt|sachverhalt|begründung)\s*:?\s*\n?', '', justification, flags=re.IGNORECASE).strip()
|
|
||||||
else:
|
|
||||||
before_justification = text
|
|
||||||
justification = ""
|
|
||||||
|
|
||||||
# Extract title (first line or first paragraph)
|
|
||||||
title_match = re.match(r'^(.+?)(?:\n\n|\n|$)', before_justification)
|
|
||||||
if title_match:
|
|
||||||
title = title_match.group(1).strip()
|
|
||||||
demand = before_justification[len(title):].strip()
|
|
||||||
else:
|
|
||||||
lines = before_justification.split('\n', 1)
|
|
||||||
title = lines[0].strip()
|
|
||||||
demand = lines[1].strip() if len(lines) > 1 else ""
|
|
||||||
|
|
||||||
# Remove title from demand if it's duplicated
|
|
||||||
if demand.startswith(title):
|
|
||||||
demand = demand[len(title):].strip()
|
|
||||||
|
|
||||||
# Remove markdown from each part
|
|
||||||
title = self._remove_markdown(title)
|
|
||||||
demand = self._remove_markdown(demand)
|
|
||||||
justification = self._remove_markdown(justification)
|
|
||||||
|
|
||||||
# Final cleanup: remove any remaining "Sachverhalt" or "Begründung/Sachverhalt" at the start
|
|
||||||
justification = re.sub(r'^(begründung\s*/?\s*sachverhalt|sachverhalt|begründung)\s*:?\s*\n?\s*', '', justification, flags=re.IGNORECASE).strip()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'title': title,
|
|
||||||
'demand': demand,
|
|
||||||
'justification': justification
|
|
||||||
}
|
|
||||||
|
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
"""Generate text from user input using Gemini API"""
|
"""Generate text from user input using Gemini API"""
|
||||||
try:
|
try:
|
||||||
if not self.model:
|
if not self.model:
|
||||||
resp.status = falcon.HTTP_500
|
resp.status = falcon.HTTP_500
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = "application/json"
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps(
|
||||||
'success': False,
|
{"success": False, "error": "Gemini API key not configured"}
|
||||||
'error': 'Gemini API key not configured'
|
)
|
||||||
})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get form data - try multiple methods for Falcon compatibility
|
# Get form data - try multiple methods for Falcon compatibility
|
||||||
anliegen = ''
|
anliegen = ""
|
||||||
party_id = ''
|
party_id = ""
|
||||||
|
|
||||||
# Method 1: Try get_param (works for URL-encoded form data)
|
# Method 1: Try get_param (works for URL-encoded form data)
|
||||||
anliegen = req.get_param('anliegen', default='') or ''
|
anliegen = req.get_param("anliegen", default="") or ""
|
||||||
party_id = req.get_param('party_id', default='') or ''
|
party_id = req.get_param("party_id", default="") or ""
|
||||||
|
|
||||||
# Method 2: If empty, try to read from stream and parse manually
|
# Method 2: If empty, try to read from stream and parse manually
|
||||||
if not anliegen:
|
if not anliegen:
|
||||||
try:
|
try:
|
||||||
# Read the raw body - use bounded_stream if available, otherwise stream
|
# Read the raw body - use bounded_stream if available, otherwise stream
|
||||||
stream = getattr(req, 'bounded_stream', req.stream)
|
stream = getattr(req, "bounded_stream", req.stream)
|
||||||
raw_body = stream.read().decode('utf-8')
|
raw_body = stream.read().decode("utf-8")
|
||||||
# Parse URL-encoded data manually
|
# Parse URL-encoded data manually
|
||||||
parsed = parse_qs(raw_body)
|
parsed = parse_qs(raw_body)
|
||||||
anliegen = parsed.get('anliegen', [''])[0]
|
anliegen = parsed.get("anliegen", [""])[0]
|
||||||
party_id = parsed.get('party_id', [''])[0]
|
party_id = parsed.get("party_id", [""])[0]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log the exception for debugging
|
# Log the exception for debugging
|
||||||
print(f"Error parsing form data: {e}")
|
print(f"Error parsing form data: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Remove any whitespace and check if actually empty
|
# Remove any whitespace and check if actually empty
|
||||||
anliegen = anliegen.strip() if anliegen else ''
|
anliegen = anliegen.strip() if anliegen else ""
|
||||||
party_id = party_id.strip() if party_id else ''
|
party_id = party_id.strip() if party_id else ""
|
||||||
|
|
||||||
if not anliegen:
|
if not anliegen:
|
||||||
resp.status = falcon.HTTP_400
|
resp.status = falcon.HTTP_400
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = "application/json"
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps(
|
||||||
'success': False,
|
{"success": False, "error": "Anliegen-Feld ist erforderlich"}
|
||||||
'error': 'Anliegen-Feld ist erforderlich'
|
)
|
||||||
})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create prompt for Gemini
|
# Create prompt for Gemini
|
||||||
|
|
@ -256,74 +221,91 @@ class GenerateAntragResource:
|
||||||
|
|
||||||
Der Antrag soll im sachlichen, offiziellen Ton einer Fraktion verfasst sein - KEINE persönliche Anrede, KEINE "ich" oder "wir" Formulierungen. Verwende die dritte Person oder Passiv-Formulierungen.
|
Der Antrag soll im sachlichen, offiziellen Ton einer Fraktion verfasst sein - KEINE persönliche Anrede, KEINE "ich" oder "wir" Formulierungen. Verwende die dritte Person oder Passiv-Formulierungen.
|
||||||
|
|
||||||
Struktur:
|
Gib das Ergebnis ALS GÜLTIGES JSON mit den folgenden Keys zurück:
|
||||||
- Die erste Zeile ist der Antragstitel. Der Titel soll PRÄGNANT, EINFACH und EINPRÄGSAM sein - maximal 8-10 Wörter. Vermeide komplizierte Formulierungen, technische Fachbegriffe oder zu lange Titel. Der Titel soll eine gute Außenwirkung haben und das Anliegen klar und verständlich kommunizieren. Beispiele für gute Titel: "Nachtabsenkung der öffentlichen Straßenbeleuchtung", "Vielfalt in Bewegung – Kulturelle Begleitmaßnahmen World Games 2029", "Prüfung digitaler Zahlungsdienstleister und WERO-Alternative"
|
{
|
||||||
- Der zweite Absatz ist der Forderungsteil. Je nachdem: Entweder Sätze und oder Liste von Forderungen.
|
"antragstitel": "PRÄGNANTER Titel (max. 8-10 Wörter)",
|
||||||
- Der letzte Teil ist Begründung/Sachverhalt (ohne diesen Titel im Text)
|
"forderung": "Forderungstext oder Liste von Forderungen",
|
||||||
|
"begruendung": "Begründung/Sachverhalt",
|
||||||
|
"mail_recipient": "Empfänger-E-Mail (z.B. 'fraktion@example.com')",
|
||||||
|
"mail_subject": "Betreff für die E-Mail",
|
||||||
|
"mail_body": "Höflicher E-Mail-Text in der ersten Person",
|
||||||
|
"filename": "Dateiname (z.B. 'antragsentwurf_...docx')"
|
||||||
|
}
|
||||||
|
|
||||||
WICHTIG:
|
WICHTIG:
|
||||||
- Reinen Text, verwende KEINE Markdown-Formatierung oder sonstige Formatierungen, ausgenommen Listen und Aufzählungen.
|
- ANTWORTE NUR MIT GÜLTIGEM JSON, KEINERLEI ERKLÄRUNGEN ODER ZUSÄTZLICHEN TEXT!
|
||||||
- Sachlicher, offizieller Ton einer Fraktion, keine persönlichen Formulierungen.
|
- Der Titel soll PRÄGNANT, EINFACH und EINPRÄGSAM sein - maximal 8-10 Wörter.
|
||||||
|
- Der Dateiname soll knapp sein, z.B. 'antragsentwurf_...docx'.
|
||||||
|
- Keine Markdown-Formatierung im Text.
|
||||||
|
- Sachlicher, offizieller Ton einer Fraktion, keine persönlichen Formulierungen im Antrag.
|
||||||
|
- Der E-Mail-Text soll persönlich sein (ich-Form).
|
||||||
|
|
||||||
"""
|
Anliegen: """
|
||||||
prompt += anliegen
|
prompt += anliegen
|
||||||
|
|
||||||
# Call Gemini API
|
# Call Gemini API
|
||||||
response = self.model.generate_content(prompt)
|
response = self.model.generate_content(prompt)
|
||||||
generated_text = response.text
|
generated_text = response.text
|
||||||
|
|
||||||
# Parse the response
|
# Log the raw response for debugging
|
||||||
parsed = self._parse_gemini_response(generated_text)
|
logger.debug(f"Gemini raw response: {generated_text}")
|
||||||
|
|
||||||
# Generate email text
|
# Extract JSON from markdown code block if present
|
||||||
email_prompt = f"""Erstelle einen kurzen, höflichen E-Mail-Text in der ERSTEN PERSON (persönlich, ich-rede) an eine Fraktion.
|
if generated_text.startswith('```json'):
|
||||||
Die E-Mail soll:
|
generated_text = generated_text[7:].strip() # Remove ```json
|
||||||
- Mit "Guten Tag," beginnen
|
if generated_text.endswith('```'):
|
||||||
- Das Anliegen kurz erklären und prägnant
|
generated_text = generated_text[:-3].strip() # Remove trailing ```
|
||||||
- Erwähnen, dass eine Antragsvorlage im Anhang beigefügt ist
|
elif generated_text.startswith('```'):
|
||||||
- Mit "Mit freundlichen Grüßen," enden
|
generated_text = generated_text[3:].strip() # Remove leading ```
|
||||||
- Verwende KEINE Markdown-Formatierung
|
if generated_text.endswith('```'):
|
||||||
- Schreibe keinen Betreff-Entwurf dazu
|
generated_text = generated_text[:-3].strip() # Remove trailing ```
|
||||||
|
|
||||||
Anliegen: {anliegen}
|
# Parse the JSON response
|
||||||
"""
|
gemini_data = json.loads(generated_text)
|
||||||
|
logger.debug(f"Parsed Gemini data: {gemini_data}")
|
||||||
email_response = self.model.generate_content(email_prompt)
|
parsed = {
|
||||||
email_text = self._remove_markdown(email_response.text)
|
"title": gemini_data.get("antragstitel", ""),
|
||||||
|
"demand": gemini_data.get("forderung", ""),
|
||||||
|
"justification": gemini_data.get("begruendung", ""),
|
||||||
|
"mail_recipient": gemini_data.get("mail_recipient", ""),
|
||||||
|
"mail_subject": gemini_data.get("mail_subject", ""),
|
||||||
|
"mail_body": gemini_data.get("mail_body", ""),
|
||||||
|
"filename": gemini_data.get("filename", ""),
|
||||||
|
}
|
||||||
|
email_text = parsed["mail_body"]
|
||||||
|
|
||||||
# Ensure proper format - clean up and ensure structure
|
# Ensure proper format - clean up and ensure structure
|
||||||
email_text = email_text.strip()
|
email_text = email_text.strip()
|
||||||
|
|
||||||
# Return JSON with the generated text parts
|
# Return JSON with the generated text parts
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = "application/json"
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps(
|
||||||
'success': True,
|
{
|
||||||
'title': parsed['title'],
|
"success": True,
|
||||||
'demand': parsed['demand'],
|
"title": parsed["title"],
|
||||||
'justification': parsed['justification'],
|
"demand": parsed["demand"],
|
||||||
'email_body': email_text,
|
"justification": parsed["justification"],
|
||||||
'party_name': party_id if party_id else ""
|
"email_body": email_text,
|
||||||
})
|
"party_name": party_id if party_id else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
logger.error(f"Error generating antrag: {str(e)}", exc_info=True)
|
||||||
traceback.print_exc()
|
|
||||||
resp.status = falcon.HTTP_500
|
resp.status = falcon.HTTP_500
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = "application/json"
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps({"success": False, "error": str(e)})
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
class GenerateWordResource:
|
class GenerateWordResource:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Get template path
|
# Get template path
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
self.template_path = os.path.join(script_dir, 'assets', 'antrag_vorlage.docx')
|
self.template_path = os.path.join(script_dir, "assets", "antrag_vorlage.docx")
|
||||||
# Fallback if not in assets
|
# Fallback if not in assets
|
||||||
if not os.path.exists(self.template_path):
|
if not os.path.exists(self.template_path):
|
||||||
assets_dir = os.path.join(script_dir, '..', 'assets')
|
assets_dir = os.path.join(script_dir, "..", "assets")
|
||||||
self.template_path = os.path.join(assets_dir, 'antrag_vorlage.docx')
|
self.template_path = os.path.join(assets_dir, "antrag_vorlage.docx")
|
||||||
|
|
||||||
def _generate_word(self, title, demand, justification, party_name=""):
|
def _generate_word(self, title, demand, justification, party_name=""):
|
||||||
"""Generate a Word document using the template"""
|
"""Generate a Word document using the template"""
|
||||||
|
|
@ -347,42 +329,42 @@ class GenerateWordResource:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Replace FRAKTION
|
# Replace FRAKTION
|
||||||
if 'FRAKTION' in full_text:
|
if "FRAKTION" in full_text:
|
||||||
for run in paragraph.runs:
|
for run in paragraph.runs:
|
||||||
if 'FRAKTION' in run.text:
|
if "FRAKTION" in run.text:
|
||||||
run.text = run.text.replace('FRAKTION', party_name)
|
run.text = run.text.replace("FRAKTION", party_name)
|
||||||
|
|
||||||
# Replace XX.XX.XXXX with current date
|
# Replace XX.XX.XXXX with current date
|
||||||
if 'XX.XX.XXXX' in full_text:
|
if "XX.XX.XXXX" in full_text:
|
||||||
for run in paragraph.runs:
|
for run in paragraph.runs:
|
||||||
if 'XX.XX.XXXX' in run.text:
|
if "XX.XX.XXXX" in run.text:
|
||||||
run.text = run.text.replace('XX.XX.XXXX', current_date)
|
run.text = run.text.replace("XX.XX.XXXX", current_date)
|
||||||
|
|
||||||
# Replace ANTRAGSTITEL (bold)
|
# Replace ANTRAGSTITEL (bold)
|
||||||
if 'ANTRAGSTITEL' in full_text:
|
if "ANTRAGSTITEL" in full_text:
|
||||||
paragraph.clear()
|
paragraph.clear()
|
||||||
run = paragraph.add_run(title)
|
run = paragraph.add_run(title)
|
||||||
run.bold = True
|
run.bold = True
|
||||||
|
|
||||||
# Replace ANTRAGSTEXT
|
# Replace ANTRAGSTEXT
|
||||||
if 'ANTRAGSTEXT' in full_text:
|
if "ANTRAGSTEXT" in full_text:
|
||||||
paragraph.clear()
|
paragraph.clear()
|
||||||
lines = antragtext.split('\n')
|
lines = antragtext.split("\n")
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
paragraph.add_run(line.strip())
|
paragraph.add_run(line.strip())
|
||||||
if i < len(lines) - 1:
|
if i < len(lines) - 1:
|
||||||
paragraph.add_run('\n')
|
paragraph.add_run("\n")
|
||||||
|
|
||||||
# Replace BEGRÜNDUNGSTEXT
|
# Replace BEGRÜNDUNGSTEXT
|
||||||
if 'BEGRÜNDUNGSTEXT' in full_text:
|
if "BEGRÜNDUNGSTEXT" in full_text:
|
||||||
paragraph.clear()
|
paragraph.clear()
|
||||||
lines = justification.split('\n')
|
lines = justification.split("\n")
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
paragraph.add_run(line.strip())
|
paragraph.add_run(line.strip())
|
||||||
if i < len(lines) - 1:
|
if i < len(lines) - 1:
|
||||||
paragraph.add_run('\n')
|
paragraph.add_run("\n")
|
||||||
|
|
||||||
# Check text boxes (shapes) for placeholders
|
# Check text boxes (shapes) for placeholders
|
||||||
# Text boxes are stored in the document's part relationships
|
# Text boxes are stored in the document's part relationships
|
||||||
|
|
@ -405,15 +387,17 @@ class GenerateWordResource:
|
||||||
|
|
||||||
# Search in main document body
|
# Search in main document body
|
||||||
if party_name:
|
if party_name:
|
||||||
replace_in_element(document_part.element, 'FRAKTION', party_name)
|
replace_in_element(document_part.element, "FRAKTION", party_name)
|
||||||
|
|
||||||
# Also search in header and footer parts
|
# Also search in header and footer parts
|
||||||
for rel in document_part.rels.values():
|
for rel in document_part.rels.values():
|
||||||
if 'header' in rel.target_ref or 'footer' in rel.target_ref:
|
if "header" in rel.target_ref or "footer" in rel.target_ref:
|
||||||
try:
|
try:
|
||||||
header_footer_part = rel.target_part
|
header_footer_part = rel.target_part
|
||||||
if party_name:
|
if party_name:
|
||||||
replace_in_element(header_footer_part.element, 'FRAKTION', party_name)
|
replace_in_element(
|
||||||
|
header_footer_part.element, "FRAKTION", party_name
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -430,38 +414,40 @@ class GenerateWordResource:
|
||||||
if not full_text:
|
if not full_text:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if party_name and 'FRAKTION' in full_text:
|
if party_name and "FRAKTION" in full_text:
|
||||||
for run in paragraph.runs:
|
for run in paragraph.runs:
|
||||||
if 'FRAKTION' in run.text:
|
if "FRAKTION" in run.text:
|
||||||
run.text = run.text.replace('FRAKTION', party_name)
|
run.text = run.text.replace("FRAKTION", party_name)
|
||||||
|
|
||||||
if 'XX.XX.XXXX' in full_text:
|
if "XX.XX.XXXX" in full_text:
|
||||||
for run in paragraph.runs:
|
for run in paragraph.runs:
|
||||||
if 'XX.XX.XXXX' in run.text:
|
if "XX.XX.XXXX" in run.text:
|
||||||
run.text = run.text.replace('XX.XX.XXXX', current_date)
|
run.text = run.text.replace(
|
||||||
|
"XX.XX.XXXX", current_date
|
||||||
|
)
|
||||||
|
|
||||||
if 'ANTRAGSTITEL' in full_text:
|
if "ANTRAGSTITEL" in full_text:
|
||||||
paragraph.clear()
|
paragraph.clear()
|
||||||
run = paragraph.add_run(title)
|
run = paragraph.add_run(title)
|
||||||
run.bold = True
|
run.bold = True
|
||||||
|
|
||||||
if 'ANTRAGSTEXT' in full_text:
|
if "ANTRAGSTEXT" in full_text:
|
||||||
paragraph.clear()
|
paragraph.clear()
|
||||||
lines = antragtext.split('\n')
|
lines = antragtext.split("\n")
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
paragraph.add_run(line.strip())
|
paragraph.add_run(line.strip())
|
||||||
if i < len(lines) - 1:
|
if i < len(lines) - 1:
|
||||||
paragraph.add_run('\n')
|
paragraph.add_run("\n")
|
||||||
|
|
||||||
if 'BEGRÜNDUNGSTEXT' in full_text:
|
if "BEGRÜNDUNGSTEXT" in full_text:
|
||||||
paragraph.clear()
|
paragraph.clear()
|
||||||
lines = justification.split('\n')
|
lines = justification.split("\n")
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
paragraph.add_run(line.strip())
|
paragraph.add_run(line.strip())
|
||||||
if i < len(lines) - 1:
|
if i < len(lines) - 1:
|
||||||
paragraph.add_run('\n')
|
paragraph.add_run("\n")
|
||||||
|
|
||||||
# Save to buffer
|
# Save to buffer
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
|
|
@ -474,29 +460,28 @@ class GenerateWordResource:
|
||||||
try:
|
try:
|
||||||
if not DOCX_AVAILABLE:
|
if not DOCX_AVAILABLE:
|
||||||
resp.status = falcon.HTTP_500
|
resp.status = falcon.HTTP_500
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = "application/json"
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps(
|
||||||
'success': False,
|
{"success": False, "error": "python-docx not installed"}
|
||||||
'error': 'python-docx not installed'
|
)
|
||||||
})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
title = req.get_param('title', default='') or ''
|
title = req.get_param("title", default="") or ""
|
||||||
demand = req.get_param('demand', default='') or ''
|
demand = req.get_param("demand", default="") or ""
|
||||||
justification = req.get_param('justification', default='') or ''
|
justification = req.get_param("justification", default="") or ""
|
||||||
party_name = req.get_param('party_name', default='') or ''
|
party_name = req.get_param("party_name", default="") or ""
|
||||||
|
|
||||||
# If empty, try to read from stream
|
# If empty, try to read from stream
|
||||||
if not title:
|
if not title:
|
||||||
try:
|
try:
|
||||||
stream = getattr(req, 'bounded_stream', req.stream)
|
stream = getattr(req, "bounded_stream", req.stream)
|
||||||
raw_body = stream.read().decode('utf-8')
|
raw_body = stream.read().decode("utf-8")
|
||||||
parsed = parse_qs(raw_body)
|
parsed = parse_qs(raw_body)
|
||||||
title = parsed.get('title', [''])[0]
|
title = parsed.get("title", [""])[0]
|
||||||
demand = parsed.get('demand', [''])[0]
|
demand = parsed.get("demand", [""])[0]
|
||||||
justification = parsed.get('justification', [''])[0]
|
justification = parsed.get("justification", [""])[0]
|
||||||
party_name = parsed.get('party_name', [''])[0]
|
party_name = parsed.get("party_name", [""])[0]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -504,31 +489,31 @@ class GenerateWordResource:
|
||||||
word_buffer = self._generate_word(title, demand, justification, party_name)
|
word_buffer = self._generate_word(title, demand, justification, party_name)
|
||||||
|
|
||||||
# Return Word document
|
# Return Word document
|
||||||
resp.content_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
resp.content_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
resp.set_header('Content-Disposition', 'attachment; filename="antrag.docx"')
|
resp.set_header("Content-Disposition", 'attachment; filename="antrag.docx"')
|
||||||
resp.data = word_buffer.read()
|
resp.data = word_buffer.read()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
resp.status = falcon.HTTP_500
|
resp.status = falcon.HTTP_500
|
||||||
resp.content_type = 'application/json'
|
resp.content_type = "application/json"
|
||||||
resp.text = json.dumps({
|
resp.text = json.dumps({"success": False, "error": str(e)})
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
class RobotsResource:
|
class RobotsResource:
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
resp.content_type = 'text/plain; charset=utf-8'
|
resp.content_type = "text/plain; charset=utf-8"
|
||||||
resp.text = f"""User-agent: *
|
resp.text = f"""User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
Sitemap: {SITE_BASE_URL}/sitemap.xml
|
Sitemap: {SITE_BASE_URL}/sitemap.xml
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SitemapResource:
|
class SitemapResource:
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
resp.content_type = 'application/xml; charset=utf-8'
|
resp.content_type = "application/xml; charset=utf-8"
|
||||||
resp.text = f"""<?xml version="1.0" encoding="UTF-8"?>
|
resp.text = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url><loc>{SITE_BASE_URL}/</loc></url>
|
<url><loc>{SITE_BASE_URL}/</loc></url>
|
||||||
|
|
@ -537,25 +522,26 @@ class SitemapResource:
|
||||||
</urlset>
|
</urlset>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Create Falcon application
|
# Create Falcon application
|
||||||
app = falcon.App()
|
app = falcon.App()
|
||||||
|
|
||||||
# Discover static assets directory
|
# Discover static assets directory
|
||||||
STATIC_DIR = os.environ.get('MEINANTRAG_STATIC_DIR')
|
STATIC_DIR = os.environ.get("MEINANTRAG_STATIC_DIR")
|
||||||
if not STATIC_DIR:
|
if not STATIC_DIR:
|
||||||
# Prefer local assets folder in development (relative to this file)
|
# Prefer local assets folder in development (relative to this file)
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
candidate = os.path.join(script_dir, 'assets')
|
candidate = os.path.join(script_dir, "assets")
|
||||||
if os.path.isdir(candidate):
|
if os.path.isdir(candidate):
|
||||||
STATIC_DIR = candidate
|
STATIC_DIR = candidate
|
||||||
else:
|
else:
|
||||||
# Try current working directory (useful when running packaged binary from project root)
|
# Try current working directory (useful when running packaged binary from project root)
|
||||||
cwd_candidate = os.path.join(os.getcwd(), 'assets')
|
cwd_candidate = os.path.join(os.getcwd(), "assets")
|
||||||
if os.path.isdir(cwd_candidate):
|
if os.path.isdir(cwd_candidate):
|
||||||
STATIC_DIR = cwd_candidate
|
STATIC_DIR = cwd_candidate
|
||||||
else:
|
else:
|
||||||
# Fallback to packaged location under share
|
# Fallback to packaged location under share
|
||||||
STATIC_DIR = os.path.join(script_dir, '..', 'share', 'meinantrag', 'assets')
|
STATIC_DIR = os.path.join(script_dir, "..", "share", "meinantrag", "assets")
|
||||||
|
|
||||||
# Add routes
|
# Add routes
|
||||||
meinantrag = MeinAntragApp()
|
meinantrag = MeinAntragApp()
|
||||||
|
|
@ -566,24 +552,24 @@ generate_word = GenerateWordResource()
|
||||||
robots = RobotsResource()
|
robots = RobotsResource()
|
||||||
sitemap = SitemapResource()
|
sitemap = SitemapResource()
|
||||||
|
|
||||||
app.add_route('/', meinantrag)
|
app.add_route("/", meinantrag)
|
||||||
app.add_route('/impressum', impressum)
|
app.add_route("/impressum", impressum)
|
||||||
app.add_route('/datenschutz', datenschutz)
|
app.add_route("/datenschutz", datenschutz)
|
||||||
app.add_route('/api/generate-antrag', generate_antrag)
|
app.add_route("/api/generate-antrag", generate_antrag)
|
||||||
app.add_route('/api/generate-word', generate_word)
|
app.add_route("/api/generate-word", generate_word)
|
||||||
app.add_route('/robots.txt', robots)
|
app.add_route("/robots.txt", robots)
|
||||||
app.add_route('/sitemap.xml', sitemap)
|
app.add_route("/sitemap.xml", sitemap)
|
||||||
|
|
||||||
# Static file route
|
# Static file route
|
||||||
if STATIC_DIR and os.path.isdir(STATIC_DIR):
|
if STATIC_DIR and os.path.isdir(STATIC_DIR):
|
||||||
app.add_static_route('/static', STATIC_DIR)
|
app.add_static_route("/static", STATIC_DIR)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
import wsgiref.simple_server
|
import wsgiref.simple_server
|
||||||
|
|
||||||
print("Starting MeinAntrag web application...")
|
print("Starting MeinAntrag web application...")
|
||||||
print("Open your browser and navigate to: http://localhost:8000")
|
print("Open your browser and navigate to: http://localhost:8000")
|
||||||
print(f"Serving static assets from: {STATIC_DIR}")
|
print(f"Serving static assets from: {STATIC_DIR}")
|
||||||
|
|
||||||
httpd = wsgiref.simple_server.make_server('localhost', 8000, app)
|
httpd = wsgiref.simple_server.make_server("localhost", 8000, app)
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue