neue-Kennzahl-spacy #94

Merged
3019483 merged 21 commits from neue-Kennzahl-spacy into main 2025-06-29 18:23:42 +02:00
56 changed files with 12897 additions and 1439 deletions

View File

@ -26,6 +26,6 @@ def health_check():
return "OK"
# für Docker wichtig: host='0.0.0.0'
# Für Docker wichtig: host='0.0.0.0'
if __name__ == "__main__":
socketio.run(app, debug=True, host="0.0.0.0", port=5050)

View File

@ -1,11 +1,12 @@
from controller.spacy_contoller import spacy_controller
from controller.kpi_setting_controller import kpi_setting_controller
from controller.spacy_controller import spacy_controller
from controller.kpi_setting_controller import kpi_setting_controller, kpi_routes
from controller.pitch_book_controller import pitch_book_controller
from controller.progress_controller import progress_controller
def register_routes(app):
app.register_blueprint(kpi_setting_controller)
app.register_blueprint(kpi_routes)
app.register_blueprint(pitch_book_controller)
app.register_blueprint(spacy_controller)
app.register_blueprint(progress_controller)

View File

@ -2,7 +2,10 @@ from flask import Blueprint, request, jsonify
from model.database import db
from model.kpi_setting_model import KPISettingModel, KPISettingType
# Routen für /api/kpi/settings (Auslesen im Frontend)
kpi_routes = Blueprint("kpi_routes", __name__, url_prefix="/api/kpi")
# Routen für /api/kpi_setting/ (Hinzufügen, Ändern, Löschen)
kpi_setting_controller = Blueprint(
"kpi_settings", __name__, url_prefix="/api/kpi_setting"
)
@ -14,12 +17,6 @@ def get_all_kpi_settings():
return jsonify([kpi_setting.to_dict() for kpi_setting in kpi_settings]), 200
@kpi_setting_controller.route("/<int:id>", methods=["GET"])
def get_kpi_setting(id):
kpi_setting = KPISettingModel.query.get_or_404(id)
return jsonify(kpi_setting.to_dict()), 200
@kpi_setting_controller.route("/", methods=["POST"])
def create_kpi_setting():
data = request.json
@ -29,13 +26,12 @@ def create_kpi_setting():
required_fields = [
"name",
"description",
"mandatory",
"type",
"translation",
"example",
"position",
"active",
"examples",
"is_trained",
]
for field in required_fields:
if field not in data:
@ -55,13 +51,12 @@ def create_kpi_setting():
new_kpi_setting = KPISettingModel(
name=data["name"],
description=data["description"],
mandatory=data["mandatory"],
type=kpi_type,
translation=data["translation"],
example=data["example"],
position=data["position"],
active=data["active"],
examples=data.get("examples", []),
is_trained=False,
)
db.session.add(new_kpi_setting)
@ -84,9 +79,6 @@ def update_kpi_setting(id):
return jsonify({"error": "KPI Setting with this name already exists"}), 409
kpi_setting.name = data["name"]
if "description" in data:
kpi_setting.description = data["description"]
if "mandatory" in data:
kpi_setting.mandatory = data["mandatory"]
@ -100,18 +92,18 @@ def update_kpi_setting(id):
400,
)
if "translation" in data:
kpi_setting.translation = data["translation"]
if "example" in data:
kpi_setting.example = data["example"]
if "position" in data:
kpi_setting.position = data["position"]
if "active" in data:
kpi_setting.active = data["active"]
if "examples" in data:
kpi_setting.examples = data["examples"]
if "is_trained" in data:
kpi_setting.is_trained = data["is_trained"]
db.session.commit()
return jsonify(kpi_setting.to_dict()), 200
@ -154,3 +146,21 @@ def update_kpi_positions():
except Exception as e:
db.session.rollback()
return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500
@kpi_routes.route("/settings", methods=["GET"])
def get_kpi_settings():
try:
kpis = KPISettingModel.query.all()
return jsonify([k.to_dict() for k in kpis]), 200
except Exception as e:
return (
jsonify({"error": "Fehler beim Abrufen der KPIs", "details": str(e)}),
500,
)
@kpi_setting_controller.route("/<int:id>", methods=["GET"])
def get_kpi_setting(id):
kpi_setting = KPISettingModel.query.get_or_404(id)
return jsonify(kpi_setting.to_dict()), 200

View File

@ -1,93 +0,0 @@
from flask import Blueprint, request, jsonify, send_file
from io import BytesIO
from model.spacy_model import SpacyModel
import puremagic
from werkzeug.utils import secure_filename
from model.database import db
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
@spacy_controller.route("/", methods=["GET"])
def get_all_files():
files = SpacyModel.query.all()
return jsonify([file.to_dict() for file in files]), 200
@spacy_controller.route("/<int:id>", methods=["GET"])
def get_file(id):
file = SpacyModel.query.get_or_404(id)
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>/download", methods=["GET"])
def download_file(id):
file = SpacyModel.query.get_or_404(id)
return send_file(
BytesIO(file.file), download_name=file.filename, as_attachment=True
)
@spacy_controller.route("/", methods=["POST"])
def upload_file():
print(request)
if "file" not in request.files:
return jsonify({"error": "No file part in the request"}), 400
uploaded_file = request.files["file"]
if uploaded_file.filename == "":
return jsonify({"error": "No selected file"}), 400
# Read file data once
file_data = uploaded_file.read()
try:
if uploaded_file:
fileName = uploaded_file.filename or ""
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
db.session.add(new_file)
db.session.commit()
return jsonify(new_file.to_dict()), 201
except Exception as e:
print(e)
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
@spacy_controller.route("/<int:id>", methods=["PUT"])
def update_file(id):
file = SpacyModel.query.get_or_404(id)
if "file" in request.files:
uploaded_file = request.files["file"]
if uploaded_file.filename != "":
file.filename = uploaded_file.filename
# Read file data once
file_data = uploaded_file.read()
try:
if (
uploaded_file
and puremagic.from_string(file_data, mime=True) == "application/pdf"
):
file.file = file_data
except Exception as e:
print(e)
if "kpi" in request.form:
file.kpi = request.form.get("kpi")
db.session.commit()
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>", methods=["DELETE"])
def delete_file(id):
file = SpacyModel.query.get_or_404(id)
db.session.delete(file)
db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200

View File

@ -0,0 +1,149 @@
from flask import Blueprint, request, jsonify, send_file
from io import BytesIO
from model.spacy_model import SpacyModel
import puremagic
from werkzeug.utils import secure_filename
from model.database import db
import os
import requests
from model.kpi_setting_model import KPISettingModel
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
SPACY_TRAINING_URL = os.getenv("SPACY_TRAINING_URL", "http://spacy:5052/train")
SPACY_URL = os.getenv("SPACY_URL", "http://spacy:5052")
@spacy_controller.route("/train", methods=["POST"])
def trigger_training():
try:
response = requests.post(SPACY_TRAINING_URL, timeout=600)
if response.ok:
return jsonify({"message": "Training erfolgreich angestoßen."}), 200
else:
return (
jsonify({"error": "Training fehlgeschlagen", "details": response.text}),
500,
)
except Exception as e:
return (
jsonify(
{"error": "Fehler beim Senden an Trainingsservice", "details": str(e)}
),
500,
)
@spacy_controller.route("/", methods=["GET"])
def get_all_files():
files = SpacyModel.query.all()
return jsonify([file.to_dict() for file in files]), 200
@spacy_controller.route("/<int:id>", methods=["GET"])
def get_file(id):
file = SpacyModel.query.get_or_404(id)
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>/download", methods=["GET"])
def download_file(id):
file = SpacyModel.query.get_or_404(id)
return send_file(
BytesIO(file.file), download_name=file.filename, as_attachment=True
)
@spacy_controller.route("/", methods=["POST"])
def upload_file():
if "file" not in request.files:
return jsonify({"error": "No file part in the request"}), 400
uploaded_file = request.files["file"]
if uploaded_file.filename == "":
return jsonify({"error": "No selected file"}), 400
file_data = uploaded_file.read()
try:
fileName = uploaded_file.filename or ""
new_file = SpacyModel(filename=secure_filename(fileName), file=file_data)
db.session.add(new_file)
db.session.commit()
return jsonify(new_file.to_dict()), 201
except Exception as e:
print(e)
return jsonify({"error": "Invalid file format. Only PDF files are accepted"}), 400
@spacy_controller.route("/<int:id>", methods=["PUT"])
def update_file(id):
file = SpacyModel.query.get_or_404(id)
if "file" in request.files:
uploaded_file = request.files["file"]
if uploaded_file.filename != "":
file.filename = uploaded_file.filename
file_data = uploaded_file.read()
try:
if puremagic.from_string(file_data, mime=True) == "application/pdf":
file.file = file_data
except Exception as e:
print(e)
if "kpi" in request.form:
file.kpi = request.form.get("kpi")
db.session.commit()
return jsonify(file.to_dict()), 200
@spacy_controller.route("/<int:id>", methods=["DELETE"])
def delete_file(id):
file = SpacyModel.query.get_or_404(id)
db.session.delete(file)
db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200
@spacy_controller.route("/append-training-entry", methods=["POST"])
def forward_training_entry():
entry = request.get_json()
try:
response = requests.post(f"{SPACY_URL}/append-training-entry", json=entry)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
# globale Variable oben einfügen
current_training_status = {"running": False}
@spacy_controller.route("/training/status", methods=["POST"])
def update_training_status():
data = request.get_json()
current_training_status["running"] = data.get("running", False)
if current_training_status["running"] is False:
try:
KPISettingModel.query.update({KPISettingModel.is_trained: True})
db.session.commit()
except Exception as e:
db.session.rollback()
return (
jsonify(
{
"error": "is_trained konnte nicht aktualisiert werden",
"details": str(e),
}
),
500,
)
return jsonify({"status": "success", "running": current_training_status["running"]})
@spacy_controller.route("/train-status", methods=["GET"])
def training_status():
return jsonify(current_training_status), 200

View File

@ -2,6 +2,8 @@ from model.database import db
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import Enum as SQLAlchemyEnum
from enum import Enum
from sqlalchemy.dialects.postgresql import JSONB
from collections import OrderedDict
class KPISettingType(Enum):
@ -18,37 +20,36 @@ class KPISettingModel(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(unique=True)
description: Mapped[str]
mandatory: Mapped[bool]
type: Mapped[KPISettingType] = mapped_column(
SQLAlchemyEnum(KPISettingType, native_enum=True)
)
translation: Mapped[str]
example: Mapped[str]
position: Mapped[int]
active: Mapped[bool]
examples: Mapped[list] = mapped_column(JSONB, default=[])
is_trained: Mapped[bool] = mapped_column(default=False)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"mandatory": self.mandatory,
"type": self.type.value,
"translation": self.translation,
"example": self.example,
"position": self.position,
"active": self.active,
}
return OrderedDict(
[
("id", self.id),
("name", self.name),
("mandatory", self.mandatory),
("type", self.type.value),
("position", self.position),
("examples", self.examples),
("active", self.active),
("is_trained", self.is_trained),
]
)
def __init__(
self, name, description, mandatory, type, translation, example, position, active
self, name, mandatory, type, position, active, examples=None, is_trained=False
):
self.name = name
self.description = description
self.mandatory = mandatory
self.type = type
self.translation = translation
self.example = example
self.position = position
self.active = active
self.examples = examples or []
self.is_trained = is_trained

View File

@ -10,153 +10,258 @@ def seed_default_kpi_settings():
default_kpi_settings = [
{
"name": "Fondsname",
"description": "Der vollständige Name des Investmentfonds",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Fund Name",
"example": "Alpha Real Estate Fund I",
"position": 1,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Der Fonds trägt den Namen Alpha Real Estate Fund I.",
"value": "Alpha Real Estate Fund I",
},
{
"sentence": "Im Pitchbook wird der Fondsname als Alpha Real Estate Fund I angegeben.",
"value": "Alpha Real Estate Fund I",
},
],
},
{
"name": "Fondsmanager",
"description": "Verantwortlicher Manager für die Fondsverwaltung",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Fund Manager",
"example": "Max Mustermann",
"position": 2,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Fondsmanager des Projekts ist Max Mustermann.",
"value": "Max Mustermann",
},
{
"sentence": "Die Verwaltung liegt bei Max Mustermann.",
"value": "Max Mustermann",
},
],
},
{
"name": "AIFM",
"description": "Alternative Investment Fund Manager",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "AIFM",
"example": "Alpha Investment Management GmbH",
"position": 3,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "AIFM ist die Alpha Investment Management GmbH.",
"value": "Alpha Investment Management GmbH",
},
{
"sentence": "Die Alpha Investment Management GmbH fungiert als AIFM.",
"value": "Alpha Investment Management GmbH",
},
],
},
{
"name": "Datum",
"description": "Stichtag der Datenerfassung",
"mandatory": True,
"type": KPISettingType.DATE,
"translation": "Date",
"example": "05.05.2025",
"position": 4,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Daten basieren auf dem Stand vom 05.05.2025.",
"value": "05.05.2025",
},
{
"sentence": "Stichtag der Angaben ist der 05.05.2025.",
"value": "05.05.2025",
},
],
},
{
"name": "Risikoprofil",
"description": "Klassifizierung des Risikos des Fonds",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Risk Profile",
"example": "Core/Core++",
"position": 5,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Der Fonds hat das Risikoprofil Core/Core++.",
"value": "Core/Core++",
},
{
"sentence": "Einstufung des Fondsrisikos: Core/Core++.",
"value": "Core/Core++",
},
],
},
{
"name": "Artikel",
"description": "Artikel 8 SFDR-Klassifizierung",
"mandatory": False,
"type": KPISettingType.BOOLEAN,
"translation": "Article",
"example": "Artikel 8",
"position": 6,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Der Fonds erfüllt die Anforderungen von Artikel 8.",
"value": "Artikel 8",
},
{
"sentence": "Gemäß SFDR fällt dieser Fonds unter Artikel 8.",
"value": "Artikel 8",
},
],
},
{
"name": "Zielrendite",
"description": "Angestrebte jährliche Rendite in Prozent",
"mandatory": True,
"type": KPISettingType.NUMBER,
"translation": "Target Return",
"example": "6.5",
"position": 7,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die angestrebte Zielrendite liegt bei 6.5%.",
"value": "6.5%",
},
{"sentence": "Zielrendite des Fonds beträgt 6.5%.", "value": "6.5%"},
],
},
{
"name": "Rendite",
"description": "Tatsächlich erzielte Rendite in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "Return",
"example": "5.8",
"position": 8,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Rendite für das Jahr beträgt 5.8%.",
"value": "5.8%",
},
{
"sentence": "Im letzten Jahr wurde eine Rendite von 5.8% erzielt.",
"value": "5.8%",
},
],
},
{
"name": "Zielausschüttung",
"description": "Geplante Ausschüttung in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "Target Distribution",
"example": "4.0",
"position": 9,
"active": True,
"is_trained": True,
"examples": [
{"sentence": "Die Zielausschüttung beträgt 4.0%.", "value": "4.0%"},
{
"sentence": "Geplante Ausschüttung: 4.0% pro Jahr.",
"value": "4.0%",
},
],
},
{
"name": "Ausschüttung",
"description": "Tatsächliche Ausschüttung in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "Distribution",
"example": "3.8",
"position": 10,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Ausschüttung im Jahr 2024 lag bei 3.8%.",
"value": "3.8%",
},
{
"sentence": "Es wurde eine Ausschüttung von 3.8% vorgenommen.",
"value": "3.8%",
},
],
},
{
"name": "Laufzeit",
"description": "Geplante Laufzeit des Fonds",
"mandatory": True,
"type": KPISettingType.STRING,
"translation": "Duration",
"example": "7 Jahre, 10, Evergreen",
"position": 11,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Laufzeit des Fonds beträgt 7 Jahre.",
"value": "7 Jahre",
},
{"sentence": "Geplante Dauer: Evergreen-Modell.", "value": "Evergreen"},
],
},
{
"name": "LTV",
"description": "Loan-to-Value Verhältnis in Prozent",
"mandatory": False,
"type": KPISettingType.NUMBER,
"translation": "LTV",
"example": "65.0",
"position": 12,
"active": True,
"is_trained": True,
"examples": [
{"sentence": "Der LTV beträgt 65.0%.", "value": "65.0%"},
{"sentence": "Loan-to-Value-Ratio: 65.0%.", "value": "65.0%"},
],
},
{
"name": "Managementgebühren",
"description": "Jährliche Verwaltungsgebühren in Prozent",
"mandatory": True,
"type": KPISettingType.NUMBER,
"translation": "Management Fees",
"example": "1.5",
"position": 13,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Managementgebühren betragen jährlich 1.5%.",
"value": "1.5%",
},
{
"sentence": "Für die Verwaltung wird eine Gebühr von 1.5% erhoben.",
"value": "1.5%",
},
],
},
{
"name": "Sektorenallokation",
"description": "Verteilung der Investments nach Sektoren",
"mandatory": False,
"type": KPISettingType.ARRAY,
"translation": "Sector Allocation",
"example": "Büro, Wohnen, Logistik, Studentenwohnen",
"position": 14,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Die Sektorenallokation umfasst Büro, Wohnen und Logistik.",
"value": "Büro, Wohnen, Logistik",
},
{
"sentence": "Investiert wird in Büro, Logistik und Studentenwohnen.",
"value": "Büro, Logistik, Studentenwohnen",
},
],
},
{
"name": "Länderallokation",
"description": "Geografische Verteilung der Investments",
"mandatory": False,
"type": KPISettingType.ARRAY,
"translation": "Country Allocation",
"example": "Deutschland,Frankreich, Österreich, Schweiz",
"position": 15,
"active": True,
"is_trained": True,
"examples": [
{
"sentence": "Investitionen erfolgen in Deutschland, Frankreich und Österreich.",
"value": "Deutschland, Frankreich, Österreich",
},
{
"sentence": "Die Länderallokation umfasst Deutschland, Schweiz und Frankreich.",
"value": "Deutschland, Schweiz, Frankreich",
},
],
},
]
@ -165,13 +270,12 @@ def seed_default_kpi_settings():
for kpi_data in default_kpi_settings:
kpi_setting = KPISettingModel(
name=kpi_data["name"],
description=kpi_data["description"],
mandatory=kpi_data["mandatory"],
type=kpi_data["type"],
translation=kpi_data["translation"],
example=kpi_data["example"],
position=kpi_data["position"],
active=kpi_data["active"],
examples=kpi_data.get("examples", []),
is_trained=kpi_data["is_trained"],
)
db.session.add(kpi_setting)

View File

@ -6,9 +6,12 @@ import json
app = Flask(__name__)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
VALIDATE_SERVICE_URL = os.getenv(
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route('/extract', methods=['POST'])
@app.route("/extract", methods=["POST"])
def extract_text_from_ocr_json():
json_data = request.get_json()
@ -16,19 +19,19 @@ def extract_text_from_ocr_json():
pages_data = json_data["extracted_text_per_page"]
entities_json = extract_with_exxeta(pages_data, pitchbook_id)
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
entities = (
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
)
validate_payload = {
"id": pitchbook_id,
"service": "exxeta",
"entities": entities
}
validate_payload = {"id": pitchbook_id, "service": "exxeta", "entities": entities}
print(f"[EXXETA] Sending to validate service: {VALIDATE_SERVICE_URL}")
print(f"[EXXETA] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try:
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
response = requests.post(
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
print(f"[EXXETA] Validate service response: {response.status_code}")
if response.status_code != 200:
print(f"[EXXETA] Validate service error: {response.text}")

View File

@ -16,6 +16,7 @@ TIMEOUT = 180
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def get_dynamic_labels():
url = f"{COORDINATOR_URL}/api/kpi_setting/"
try:
@ -28,6 +29,7 @@ def get_dynamic_labels():
logger.warning(f"Konnte dynamische Labels nicht laden: {e}")
return []
def extract_with_exxeta(pages_json, pitchbook_id):
results = []
@ -39,7 +41,10 @@ def extract_with_exxeta(pages_json, pitchbook_id):
for page_data in pages_json:
i += 1
if i % 8 == 0:
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35 + 60/len(pages_json)*i})
requests.post(
COORDINATOR_URL + "/api/progress",
json={"id": pitchbook_id, "progress": 35 + 60 / len(pages_json) * i},
)
page_num = page_data.get("page")
text = page_data.get("text", "")
@ -100,23 +105,28 @@ def extract_with_exxeta(pages_json, pitchbook_id):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {EXXETA_API_KEY}"
"Authorization": f"Bearer {EXXETA_API_KEY}",
}
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": "Du bist ein Finanzanalyst. Antworte ausschließlich mit einem validen JSON-Array."},
{"role": "user", "content": prompt}
{
"role": "system",
"content": "Du bist ein Finanzanalyst. Antworte ausschließlich mit einem validen JSON-Array.",
},
{"role": "user", "content": prompt},
],
"temperature": 0.0
"temperature": 0.0,
}
url = f"{EXXETA_BASE_URL}/deployments/{MODEL}/chat/completions"
for attempt in range(1, MAX_RETRIES + 1):
try:
response = requests.post(url, headers=headers, json=payload, timeout=TIMEOUT)
response = requests.post(
url, headers=headers, json=payload, timeout=TIMEOUT
)
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"].strip()
if content.startswith("```json"):
@ -140,9 +150,12 @@ def extract_with_exxeta(pages_json, pitchbook_id):
if attempt == MAX_RETRIES:
results.extend([])
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95})
requests.post(
COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95}
)
return json.dumps(results, indent=2, ensure_ascii=False)
if __name__ == "__main__":
print("📡 Test-Aufruf get_dynamic_labels:")
print(get_dynamic_labels())

View File

@ -29,19 +29,17 @@ def convert_pdf_async(temp_path, pitchbook_id):
temp_path.unlink() # cleanup
return {"error": "OCR processing failed - all PDFs must be OCR'd"}, 500
with open(ocr_path, 'rb') as ocr_file:
with open(ocr_path, "rb") as ocr_file:
ocr_file.seek(0)
result = pdf_to_json(ocr_file)
payload = {
"id": int(pitchbook_id),
"extracted_text_per_page": result["pages"]
}
payload = {"id": int(pitchbook_id), "extracted_text_per_page": result["pages"]}
logger.info("Sending payload to EXXETA and SPACY services")
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35})
requests.post(
COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35}
)
try:
exxeta_response = requests.post(EXXETA_URL, json=payload, timeout=600)
logger.info(f"EXXETA response: {exxeta_response.status_code}")
@ -54,14 +52,16 @@ def convert_pdf_async(temp_path, pitchbook_id):
except Exception as e:
logger.error(f"Error calling SPACY: {e}")
files=[
('file',('',open(ocr_path,'rb'),'application/pdf'))
]
files = [("file", ("", open(ocr_path, "rb"), "application/pdf"))]
headers = {}
try:
requests.put(f"{COORDINATOR_URL}/api/pitch_book/{pitchbook_id}", files=files, timeout=600, headers=headers)
requests.put(
f"{COORDINATOR_URL}/api/pitch_book/{pitchbook_id}",
files=files,
timeout=600,
headers=headers,
)
logger.info("COORDINATOR response: Progress + File updated")
except Exception as e:
logger.error(f"Error calling COORDINATOR: {e}")
@ -72,7 +72,7 @@ def convert_pdf_async(temp_path, pitchbook_id):
logger.error(f"Exception in OCR processing: {str(e)}", exc_info=True)
@app.route('/ocr', methods=['POST'])
@app.route("/ocr", methods=["POST"])
def convert_extract_text_from_pdf():
if "file" not in request.files:
return {"error": "No file"}, 400
@ -85,7 +85,7 @@ def convert_extract_text_from_pdf():
if not pitchbook_id:
return {"error": "No ID"}, 400
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
file.seek(0)
temp_file.write(file.read())
temp_path = Path(temp_file.name)
@ -93,10 +93,7 @@ def convert_extract_text_from_pdf():
thread = threading.Thread(target=convert_pdf_async, args=(temp_path, pitchbook_id))
thread.start()
return {
"status": "sent",
"message": "PDF successfully OCR'd and processed"
}, 200
return {"status": "sent", "message": "PDF successfully OCR'd and processed"}, 200
if __name__ == "__main__":

View File

@ -17,9 +17,10 @@ log_folder = TEMP_DIR / "logs"
output_folder.mkdir(exist_ok=True)
log_folder.mkdir(exist_ok=True)
def pdf_to_json(pdf_input):
try:
if hasattr(pdf_input, 'read'):
if hasattr(pdf_input, "read"):
pdf_input.seek(0)
with pdfplumber.open(pdf_input) as pdf:
@ -83,7 +84,9 @@ def ocr_pdf(input_file_path: Path):
if result.returncode == 0:
if output_file.exists():
logger.info(f"OCR successful, output file size: {output_file.stat().st_size} bytes")
logger.info(
f"OCR successful, output file size: {output_file.stat().st_size} bytes"
)
return output_file
else:
logger.error(f"OCR completed but output file not found: {output_file}")

View File

@ -11,6 +11,8 @@ COPY requirements.txt /app
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install flask-cors

requirements bitte nicht einzeln im Dockerfile installieren, du hast sie schon in der requirement.txt.

requirements bitte nicht einzeln im Dockerfile installieren, du hast sie schon in der requirement.txt.

Danke für den Hinweis – die Zeile nehme ich wieder raus, ist ja schon in der requirements.txt drin.

Danke für den Hinweis – die Zeile nehme ich wieder raus, ist ja schon in der requirements.txt drin.
RUN python -m spacy download en_core_web_sm

View File

@ -1,14 +1,26 @@
from flask import Flask, request, jsonify
from extractSpacy import extract
from extractSpacy import extract, load_model
import requests
import os
import json
from flask_cors import CORS
import shutil
import subprocess
training_status = {"running": False}
app = Flask(__name__)
CORS(app)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
COORDINATOR_URL = os.getenv("COORDINATOR_URL", "http://coordinator:5000")
VALIDATE_SERVICE_URL = os.getenv(
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route('/extract', methods=['POST'])
@app.route("/extract", methods=["POST"])
def extract_pdf():
json_data = request.get_json()
@ -16,19 +28,19 @@ def extract_pdf():
pages_data = json_data["extracted_text_per_page"]
entities_json = extract(pages_data)
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
entities = (
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
)
validate_payload = {
"id": pitchbook_id,
"service": "spacy",
"entities": entities
}
validate_payload = {"id": pitchbook_id, "service": "spacy", "entities": entities}
print(f"[SPACY] Sending to validate service: {VALIDATE_SERVICE_URL}")
print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try:
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
response = requests.post(
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
print(f"[SPACY] Validate service response: {response.status_code}")
if response.status_code != 200:
print(f"[SPACY] Validate service error: {response.text}")
@ -38,5 +50,90 @@ def extract_pdf():
return jsonify("Sent to validate-service"), 200
@app.route("/append-training-entry", methods=["POST"])
def append_training_entry():
entry = request.get_json()
if not entry or "text" not in entry or "entities" not in entry:
return (
jsonify(
{"error": "Ungültiges Format 'text' und 'entities' erforderlich."}
),
400,
)
path = os.path.join("spacy_training", "annotation_data.json")
try:
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Duplikate prüfen
if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200
data.append(entry)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200
except Exception as e:
print(f"[ERROR] Fehler beim Schreiben der Datei: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500
@app.route("/train", methods=["POST"])
def trigger_training():
from threading import Thread
Thread(target=run_training).start()
return jsonify({"message": "Training gestartet"}), 200
@app.route("/reload-model", methods=["POST"])
def reload_model():
try:
load_model()
return jsonify({"message": "Modell wurde erfolgreich neu geladen."}), 200
except Exception as e:
return (
jsonify({"error": "Fehler beim Neuladen des Modells", "details": str(e)}),
500,
)
def run_training():
training_status["running"] = True
notify_coordinator(True)
try:
if os.path.exists("output/model-last"):
shutil.copytree(
"output/model-last", "output/model-backup", dirs_exist_ok=True
)
subprocess.run(["python", "spacy_training/ner_trainer.py"], check=True)
load_model()
except Exception as e:
print("Training failed:", e)
training_status["running"] = False
notify_coordinator(False)
def notify_coordinator(running: bool):
try:
response = requests.post(
f"{COORDINATOR_URL}/api/spacy/training/status", json={"running": running}
)
print(
f"[SPACY] Coordinator: running = {running}, Status = {response.status_code}"
)
except Exception as e:
print(f"[SPACY] Fehler beim Senden des Trainingsstatus: {e}")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5052, debug=True)

View File

@ -2,9 +2,25 @@ import spacy
import os
import json
current_dir = os.path.dirname(os.path.abspath(__file__))
model_path = os.path.join(current_dir, "spacy_training/output/model-last")
nlp = spacy.load(model_path)
# Globales NLP-Modell
nlp = None
def load_model():
global nlp
print("[INFO] Lade SpaCy-Modell aus spacy_training/output/model-last ...")
nlp = spacy.load("spacy_training/output/model-last")
print("[INFO] Modell erfolgreich geladen.")
# Initial einmal laden
load_model()
def extract(pages_json):
results = []
@ -19,10 +35,6 @@ def extract(pages_json):
spacy_result = nlp(text)
for ent in spacy_result.ents:
results.append({
"label": ent.label_,
"entity": ent.text,
"page": page_num
})
results.append({"label": ent.label_, "entity": ent.text, "page": page_num})
return json.dumps(results, indent=2, ensure_ascii=False)

View File

@ -4,3 +4,4 @@ transformers==4.35.2
torch
flask
requests
flask-cors

View File

@ -0,0 +1,33 @@
from flask import Flask, request, jsonify

Was macht die datei? Ich sehe keine stelle, an die die referenziert wird und die Flask-App wird glaube ich auch nicht gestartet

Was macht die datei? Ich sehe keine stelle, an die die referenziert wird und die Flask-App wird glaube ich auch nicht gestartet

Wird aktuell gar nicht genutzt – ich hatte die Datei nur kurz verwendet, um Trainingsdaten von einer Python-Liste in JSON umzuwandeln. Kann gelöscht werden

Wird aktuell gar nicht genutzt – ich hatte die Datei nur kurz verwendet, um Trainingsdaten von einer Python-Liste in JSON umzuwandeln. Kann gelöscht werden
import os
import json
app = Flask(__name__)
ANNOTATION_FILE = "spacy_training/annotation_data.json"
@app.route("/api/spacy-training-entry", methods=["POST"])
def append_training_entry():
new_entry = request.get_json()
if not new_entry or "text" not in new_entry or "entities" not in new_entry:
return jsonify({"error": "Ungültiges Format"}), 400
# Bestehende Datei laden
if os.path.exists(ANNOTATION_FILE):
with open(ANNOTATION_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Duplikat vermeiden
if new_entry in data:
return jsonify({"message": "Eintrag bereits vorhanden."}), 200
# Anfügen
data.append(new_entry)
with open(ANNOTATION_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +0,0 @@
# This is an auto-generated partial config. To use it with 'spacy train'
# you can run spacy init fill-config to auto-fill all default settings:
# python -m spacy init fill-config ./base_config.cfg ./config.cfg
[paths]
train = ./data/train.spacy
dev = ./data/train.spacy
vectors = null
[system]
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["tok2vec","ner"]
batch_size = 1000
[components]
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"]
rows = [5000, 1000, 2500, 2500]
include_static_vectors = false
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
width = 96
depth = 4
window_size = 1
maxout_pieces = 3
[components.ner]
factory = "ner"
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
[corpora]
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
[training.optimizer]
@optimizers = "Adam.v1"
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
[initialize]
vectors = ${paths.vectors}

View File

@ -0,0 +1,18 @@
import json

Die Datei wird glaube ich auch nirgends verwendet

Die Datei wird glaube ich auch nirgends verwendet

Wird aktuell gar nicht genutzt – kann raus, war nur ein Testfile. Lösch ich direkt.

Wird aktuell gar nicht genutzt – kann raus, war nur ein Testfile. Lösch ich direkt.
# Alte Daten laden
with open("annotation_data.json", "r", encoding="utf-8") as f:
data = json.load(f)
# Neue Kennzahl (als Dict/Objekt)
neuer_eintrag = {
"text": "Hier steht der Beispielsatz mit der neuen Kennzahl.",
"entities": [[1, 5, "NEUEKENNZAHL"]],
}
# Anhängen
data.append(neuer_eintrag)
# Wieder speichern
with open("annotation_data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)

View File

@ -0,0 +1,81 @@
import spacy
from spacy.training.example import Example
import json
import os
import shutil
import sys
def load_data(file_path):
with open(file_path, "r", encoding="utf8") as f:
raw = json.load(f)
return [
(
entry["text"],
{
"entities": [
(start, end, label) for start, end, label in entry["entities"]
]
},
)
for entry in raw
]
def main():
# Stelle sicher, dass der "output"-Ordner existiert
os.makedirs("output", exist_ok=True)
TRAIN_DATA = load_data(os.path.join("spacy_training", "annotation_data.json"))
nlp = spacy.blank("de")
1924466 marked this conversation as resolved

Kommentierter Code auch gerne weg. Auch an anderen paar stellen ;)

Kommentierter Code auch gerne weg. Auch an anderen paar stellen ;)
ner = nlp.add_pipe("ner")
ner.add_label("KENNZAHL")
optimizer = nlp.begin_training()
for i in range(20):
for text, annotations in TRAIN_DATA:
example = Example.from_dict(nlp.make_doc(text), annotations)
nlp.update([example], drop=0.2, sgd=optimizer)
temp_model_dir = "output/temp-model"
final_model_dir = "output/model-last"
backup_dir = "output/model-backup"
try:
# Vorheriges temporäres Verzeichnis entfernen
if os.path.exists(temp_model_dir):
shutil.rmtree(temp_model_dir)
# Modell zunächst in temp speichern
nlp.to_disk(temp_model_dir)
# Backup der letzten Version (falls vorhanden)
if os.path.exists(final_model_dir):
if os.path.exists(backup_dir):
shutil.rmtree(backup_dir)
shutil.copytree(final_model_dir, backup_dir)
shutil.rmtree(final_model_dir)
# Modell verschieben
shutil.move(temp_model_dir, final_model_dir)
print("[INFO] Training abgeschlossen und Modell gespeichert.")
nlp.to_disk("spacy_training/output/model-last")
# Training beendet Status auf False setzen
with open("spacy_training/training_running.json", "w") as f:
json.dump({"running": False}, f)
sys.exit(0)
except Exception as e:
print(f"[FEHLER] Während des Trainings ist ein Fehler aufgetreten: {e}")
if os.path.exists(temp_model_dir):
shutil.rmtree(temp_model_dir)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,21 +1,21 @@
[paths]
train = "./data/train.spacy"
dev = "./data/train.spacy"
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
gpu_allocator = null
seed = 0
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["tok2vec","ner"]
batch_size = 1000
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
@ -38,51 +38,34 @@ use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
upstream = "*"
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
rows = [5000,1000,2500,2500]
include_static_vectors = false
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
@ -93,6 +76,8 @@ max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null

View File

@ -0,0 +1,63 @@
{
"lang":"de",
"name":"pipeline",
"version":"0.0.0",
"spacy_version":">=3.8.7,<3.9.0",
"description":"",
"author":"",
"email":"",
"url":"",
"license":"",
"spacy_git_version":"4b65aa7",
"vectors":{
"width":0,
"vectors":0,
"keys":0,
"name":null,
"mode":"default"
},
"labels":{
"ner":[
"AUSSCH\u00dcTTUNGSRENDITE",
"ESFCG",
"HHGK",
"KENNZAHL",
"LAUFZEIT",
"LAUFZEIT2",
"L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN",
"RENDITE",
"RISIKOPROFIL",
"SEKTORENALLOKATION",
"TEST",
"TEST3",
"TEST323",
"TEST33",
"TEST34",
"TEST345",
"TEST35",
"TEST36",
"TEST37",
"TEST4",
"TEST44",
"TEST45",
"TEST5",
"TEST65",
"TEST67",
"TEST88",
"TEST99",
"ZIELAUSSCH\u00dcTTUNG",
"ZIELRENDITE",
"ZZGER 33"
]
},
"pipeline":[
"ner"
],
"components":[
"ner"
],
"disabled":[
]
}

View File

@ -0,0 +1,13 @@
{
"moves":null,
"update_with_oracle_cut_size":100,
"multitasks":[
],
"min_action_freq":1,
"learn_tokens":false,
"beam_width":1,
"beam_density":0.0,
"beam_update_prob":0.0,
"incorrect_spans_key":null
}

View File

@ -0,0 +1 @@
¥movesÚÈ{"0":{},"1":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"2":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"3":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"4":{"":1,"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"5":{"":1}}£cfg<66>§neg_keyÀ

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
{
"mode":"default"
}

View File

@ -1,21 +1,21 @@
[paths]
train = "./data/train.spacy"
dev = "./data/train.spacy"
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
gpu_allocator = null
seed = 0
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["tok2vec","ner"]
batch_size = 1000
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
@ -38,51 +38,34 @@ use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.Tok2VecListener.v1"
width = ${components.tok2vec.model.encode.width}
upstream = "*"
[components.tok2vec]
factory = "tok2vec"
[components.tok2vec.model]
@architectures = "spacy.Tok2Vec.v2"
[components.tok2vec.model.embed]
@architectures = "spacy.MultiHashEmbed.v2"
width = ${components.tok2vec.model.encode.width}
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
rows = [5000,1000,2500,2500]
include_static_vectors = false
[components.tok2vec.model.encode]
@architectures = "spacy.MaxoutWindowEncoder.v2"
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
@ -93,6 +76,8 @@ max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null

View File

@ -17,11 +17,9 @@
"mode":"default"
},
"labels":{
"tok2vec":[
],
"ner":[
"AUSSCH\u00dcTTUNGSRENDITE",
"KENNZAHL",
"LAUFZEIT",
"L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN",
@ -33,68 +31,12 @@
]
},
"pipeline":[
"tok2vec",
"ner"
],
"components":[
"tok2vec",
"ner"
],
"disabled":[
],
"performance":{
"ents_f":0.9608938547,
"ents_p":1.0,
"ents_r":0.9247311828,
"ents_per_type":{
"RISIKOPROFIL":{
"p":1.0,
"r":1.0,
"f":1.0
},
"AUSSCH\u00dcTTUNGSRENDITE":{
"p":1.0,
"r":0.5925925926,
"f":0.7441860465
},
"LAUFZEIT":{
"p":1.0,
"r":1.0,
"f":1.0
},
"RENDITE":{
"p":1.0,
"r":1.0,
"f":1.0
},
"L\u00c4NDERALLOKATION":{
"p":1.0,
"r":0.8965517241,
"f":0.9454545455
},
"ZIELRENDITE":{
"p":1.0,
"r":1.0,
"f":1.0
},
"ZIELAUSSCH\u00dcTTUNG":{
"p":1.0,
"r":1.0,
"f":1.0
},
"MANAGMENTGEB\u00dcHREN":{
"p":1.0,
"r":1.0,
"f":1.0
},
"SEKTORENALLOKATION":{
"p":1.0,
"r":1.0,
"f":1.0
}
},
"tok2vec_loss":33.6051129291,
"ner_loss":740.5764770508
}
]
}

View File

@ -1 +1 @@
¥movesÚL{"0":{},"1":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"2":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"3":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2},"4":{"RISIKOPROFIL":161,"L\u00c4NDERALLOKATION":161,"RENDITE":91,"AUSSCH\u00dcTTUNGSRENDITE":68,"LAUFZEIT":38,"ZIELRENDITE":12,"SEKTORENALLOKATION":12,"MANAGMENTGEB\u00dcHREN":8,"ZIELAUSSCH\u00dcTTUNG":2,"":1},"5":{"":1}}£cfg<66>§neg_keyÀ
¥movesÚˆ{"0":{},"1":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"2":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"3":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"4":{"":1,"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10},"5":{"":1}}£cfg<66>§neg_keyÀ

View File

@ -52,9 +52,7 @@
"*",
"+",
"+/D",
"+/d",
"+AU",
"+au",
",",
",00",
",03",
@ -129,21 +127,15 @@
"/080%/212%/491",
"/2,12",
"/3",
"/AuM",
"/Core+",
"/FK",
"/XX",
"/XxX",
"/Xxxx+",
"/aum",
"/core+",
"/d",
"/d,dd",
"/ddd%/ddd%/ddd",
"/fk",
"/xx",
"/xxx",
"/xxxx+",
"0",
"0%+",
"0,0",
@ -281,7 +273,6 @@
"45",
"491",
"5",
"5%+",
"5,0",
"5,00",
"5,1",
@ -311,7 +302,6 @@
"7",
"7,1",
"7,5",
"7,5%+",
"7,50",
"7,50%+",
"7.5",
@ -328,8 +318,6 @@
"8-D",
"8-d",
"80",
"800",
"84,0",
"85",
"8D",
"8d",
@ -469,7 +457,6 @@
"Abteilung",
"Access",
"Add",
"Agreements",
"Aktive",
"Aktueller",
"AlF",
@ -477,7 +464,6 @@
"Allocation",
"Allokation",
"Allokationsprofil",
"Alternative",
"Amsterdam",
"Andere",
"Anfrage",
@ -513,7 +499,6 @@
"Artikel",
"Asset",
"Assets",
"AuM",
"Aufbau",
"Auflage",
"Aufl\u00f6sung",
@ -583,10 +568,7 @@
"COR",
"CORE",
"CSU",
"CSp",
"Cash",
"Cash-Flow-Stabilit\u00e4t",
"Cash-flow",
"Chr",
"Chr.",
"Cie",
@ -648,7 +630,6 @@
"E.",
"EAN",
"ECLF",
"EIT",
"EM",
"ERD",
"ESG-",
@ -683,7 +664,6 @@
"F",
"F.",
"FDR",
"FIL",
"FR",
"FRANCE",
"FUND",
@ -794,11 +774,9 @@
"III.",
"INK",
"INREV",
"ION",
"IRR",
"IRR6.5",
"IT",
"ITE",
"IUM",
"IV",
"IV.",
@ -824,7 +802,6 @@
"Interner",
"Invastitionsfokus",
"Investftionsvolumen",
"Investing",
"Investitionen",
"Investitions-annahmen",
"Investitionsphase",
@ -834,7 +811,6 @@
"Investmentzeitraum",
"Investoren",
"Investtionszeltraum",
"Investtionszeltraum,10",
"Investtonszeltraum",
"Ireland",
"Irland",
@ -866,6 +842,7 @@
"K.",
"K.O.",
"KAGB",
"KENNZAHL",
"KINGDOM",
"KVG",
"Kapitalstruktur",
@ -905,7 +882,6 @@
"Light",
"Limited",
"Lisbon",
"Loan-to-Value",
"Local",
"Logistics",
"Logistik",
@ -1056,12 +1032,10 @@
"Prof",
"Prof.",
"Professor",
"Prognose",
"Prognostiderte",
"Prognostizierte",
"Projektentwicklungen",
"Projektentwicklungsrisiken",
"PropCo",
"Pt",
"Punkt",
"Q",
@ -1072,7 +1046,6 @@
"R.",
"R.I.P.",
"RE",
"REN",
"RENDITE",
"REV",
"REWE",
@ -1104,7 +1077,6 @@
"S",
"S'",
"SCS",
"SCSp",
"SEKTORENALLOKATION",
"SFDR",
"SG-",
@ -1197,7 +1169,6 @@
"U.S.S.",
"UK",
"UND",
"UNG",
"UNITED",
"USt",
"Univ",
@ -1264,7 +1235,6 @@
"XXXX",
"XXXX-XXXX",
"XXXd.d",
"XXXx",
"XXXxx",
"XXx",
"XXxxxx",
@ -1289,18 +1259,15 @@
"Xxxx-XXX",
"Xxxx-Xxxx-Xxxxx",
"Xxxx-Xxxxx-XXX",
"Xxxx-xx-Xxxxx",
"Xxxx-xxx",
"Xxxx-xxxx",
"Xxxx.",
"Xxxx.-Xxx",
"Xxxx.-Xxx.",
"XxxxXx",
"Xxxxx",
"Xxxxx)-",
"Xxxxx)/Xxxx",
"Xxxxx+",
"Xxxxx,dd",
"Xxxxx-",
"Xxxxx-XXX",
"Xxxxx-XxX",
@ -1381,6 +1348,7 @@
"a.g.",
"a.m.",
"a.z.",
"a34",
"ab",
"abb",
"abb.",
@ -1410,14 +1378,11 @@
"advantage",
"ae",
"aft",
"agb",
"age",
"agreements",
"aha",
"ahe",
"ahl",
"ahr",
"aif",
"ail",
"aiming",
"ain",
@ -1429,7 +1394,6 @@
"al.",
"ald",
"ale",
"alf",
"all",
"allg",
"allg.",
@ -1440,8 +1404,6 @@
"allokationsprofil",
"als",
"also",
"alt",
"alternative",
"aly",
"am.",
"ambulant",
@ -1497,7 +1459,6 @@
"ary",
"as",
"ase",
"ash",
"ass",
"asset",
"assetor",
@ -1535,7 +1496,6 @@
"aussch\u00fcttungsrandite",
"aussch\u00fcttungsrendite",
"aussch\u00fcttungsrendites",
"aut",
"ave",
"ax.",
"b",
@ -1544,7 +1504,6 @@
"b.sc",
"b.sc.",
"bahnhof",
"bal",
"balanced",
"basis",
"bau",
@ -1586,7 +1545,6 @@
"both",
"bps",
"br.",
"brands",
"broad",
"brussels",
"bruttofondsverm\u00f6gens",
@ -1619,8 +1577,6 @@
"capital",
"capped",
"carbon",
"cash",
"cash-flow",
"cash-flow-stabilit\u00e4t",
"cbd",
"cdu",
@ -1643,7 +1599,6 @@
"cl.",
"class",
"cle",
"clf",
"closed",
"closing",
"closings",
@ -1663,7 +1618,6 @@
"construction",
"contract",
"contracts",
"cor",
"core",
"core+",
"core+/d",
@ -1672,7 +1626,6 @@
"could",
"country",
"creation",
"csp",
"csu",
"cts",
"currency",
@ -1684,7 +1637,6 @@
"d+au",
"d+aut",
"d,d",
"d,d%+",
"d,dd",
"d,dd%+",
"d,ddd",
@ -1710,6 +1662,7 @@
"darge",
"darlehen",
"das",
"dasda34",
"dat",
"dd",
"dd,d",
@ -1735,7 +1688,6 @@
"der",
"dergleichen",
"des",
"destinations",
"deutsche",
"deutsches",
"deutschland",
@ -1756,7 +1708,6 @@
"dipl.",
"dipl.-ing",
"dipl.-ing.",
"dis",
"discretionary",
"distributions",
"diversification",
@ -1767,8 +1718,6 @@
"dle",
"do",
"do.",
"dom",
"domicile",
"domiciled",
"don",
"down",
@ -1783,7 +1732,6 @@
"durchschnittlich",
"du\u2019s",
"dv.",
"dxxx.\u20ac",
"dy",
"d\u00e4nemark",
"d\u2019",
@ -1847,7 +1795,6 @@
"eln",
"els",
"elt",
"ely",
"em",
"em.",
"emerging",
@ -1877,7 +1824,6 @@
"er.",
"erb",
"erbbaurechte",
"erd",
"ere",
"erfolgten",
"erg",
@ -1902,7 +1848,6 @@
"ete",
"etr",
"ets",
"eturn",
"eu-offenlegungsverordnung",
"eur",
"euro",
@ -1940,7 +1885,6 @@
"fam",
"fam.",
"favour",
"fdr",
"feb",
"feb.",
"fee",
@ -1952,7 +1896,6 @@
"ff",
"fierce",
"fil",
"financially",
"finanzierung",
"finanzierungskonditionen",
"finland",
@ -1960,7 +1903,6 @@
"first",
"flagship",
"flow",
"flow-oriented",
"fl\u00e4che",
"focus",
"focused",
@ -2051,7 +1993,6 @@
"gic",
"gie",
"gl.",
"global",
"globale",
"gmbh",
"goal",
@ -2231,7 +2172,6 @@
"investoren",
"investors",
"investtionszeltraum",
"investtionszeltraum,10",
"investtonszeltraum",
"inw",
"io.",
@ -2374,8 +2314,6 @@
"lls",
"llt",
"llv",
"lly",
"loan-to-value",
"local",
"locations",
"lock-in",
@ -2383,7 +2321,6 @@
"logistik",
"logistikimmobilien",
"london",
"long-term",
"low",
"lps",
"lso",
@ -2393,7 +2330,6 @@
"lto",
"ltv",
"ltv-ziel",
"lty",
"lu",
"lub",
"lue",
@ -2427,7 +2363,6 @@
"management",
"manager",
"manager-defined",
"managmentgeb\u00fchren",
"mandate",
"mandates",
"market",
@ -2439,7 +2374,6 @@
"maximal",
"maximaler",
"mbH",
"mbh",
"means",
"medizin",
"medizinnahe",
@ -2659,11 +2593,7 @@
"parformanceabh\u00e4ngige",
"paris",
"parks",
"partners",
"partnership",
"pattern",
"pci",
"pco",
"ped",
"pen",
"per",
@ -2694,18 +2624,15 @@
"pricey",
"pricing",
"prime",
"pro",
"prof",
"prof.",
"profile",
"prognose",
"prognostiderte",
"prognostizierte",
"program",
"projects",
"projektentwicklungen",
"projektentwicklungsrisiken",
"propco",
"properties",
"proprietary",
"provide",
@ -2719,7 +2646,6 @@
"q.",
"q.e.d",
"q.e.d.",
"qin",
"quality",
"quarterly",
"quota",
@ -2756,7 +2682,6 @@
"relationships",
"remains",
"ren",
"rendite",
"rendite-",
"rendite-risiko-profil",
"renegotiation",
@ -2773,7 +2698,6 @@
"retailinvestitionsvolumen",
"return",
"returns",
"rev",
"reversion",
"rewe",
"rge",
@ -2800,12 +2724,10 @@
"rop",
"rotterdam",
"rr.",
"rre",
"rs.",
"rsg",
"rst",
"rte",
"rtt",
"rz.",
"r\u00f6m",
"r\u00f6m.",
@ -2826,7 +2748,6 @@
"schweden",
"scope",
"scs",
"scsp",
"sd.",
"sector",
"sectors",
@ -2835,7 +2756,6 @@
"segment",
"sektor",
"sektoraler",
"sektorenallokation",
"selection",
"sen",
"sen.",
@ -2849,7 +2769,6 @@
"set",
"sf.",
"sfdr",
"sg-",
"sg.",
"short-term",
"sicav-raif",
@ -2870,7 +2789,6 @@
"sofern",
"sog",
"sog.",
"solely",
"solvency",
"some",
"son",
@ -3128,9 +3046,6 @@
"worldwide",
"x",
"x'",
"x+xx",
"x+xxx",
"x-xxxx",
"x.",
"x.X",
"x.X.",
@ -3157,38 +3072,23 @@
"xemoours",
"xit",
"xx",
"xx-xxxx",
"xx.",
"xx.x",
"xxXxx",
"xxx",
"xxx-",
"xxx-Xxxxx",
"xxx-xxxx",
"xxx.",
"xxxd.d",
"xxxx",
"xxxx)-",
"xxxx)/xxxx",
"xxxx+",
"xxxx+/x",
"xxxx+/xxxx",
"xxxx,dd",
"xxxx-",
"xxxx-xx",
"xxxx-xx-xxxx",
"xxxx-xxx",
"xxxx-xxxx",
"xxxx-xxxx-xxx",
"xxxx-xxxx-xxxx",
"xxxx.",
"xxxx\u0308xx",
"xxxx\u0308xxx-xxxx",
"xxxx\u0308xxxx",
"xxxxdd",
"xxxx\u2019x",
"xxx\u2019x",
"xx\u0308x",
"xx\u0308xxxx",
"xx\u2019x",
"x\u0308xxx",
"x\u0308xxxx",
@ -3224,7 +3124,6 @@
"zielallokation",
"zielanlagestrategie",
"zielausschu\u0308ttung",
"zielaussch\u00fcttung",
"zielmarkts",
"zielm\u00e4rkte",
"zielobjekte",

View File

@ -0,0 +1,122 @@
[
{
"text": "Core",
"entities": [
[
0,
4,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+",
"entities": [
[
0,
5,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Value Add",
"entities": [
[
0,
9,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Value Add",
"entities": [
[
0,
14,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+/Value Add",
"entities": [
[
0,
15,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+/Value Add",
"entities": [
[
0,
20,
"RISIKOPROFIL"
]
]
},
{
"text": "The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
"entities": [
[
82,
92,
"RISIKOPROFIL"
]
]
},
{
"text": "Risk level: Core/Core+",
"entities": [
[
12,
22,
"RISIKOPROFIL"
]
]
},
{
"text": "Different risk profile (core, core+, value-added)",
"entities": [
[
24,
48,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+ with OpCo premium",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Core /Core+ Assets, well-established = Key Gateway Cities in Europe le.g. hotels in the market with minor asset London, Paris, Amsterdam, Berlin] management initiatives",
"entities": [
[
0,
11,
"RISIKOPROFIL"
]
]
}
]

View File

@ -1,563 +0,0 @@
TRAINING_DATA = [
(
"Core",
{"entities": [[0, 4, "RISIKOPROFIL"]]},
),
(
"Core+",
{"entities": [[0, 5, "RISIKOPROFIL"]]},
),
(
"Core/Core+",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Value Add",
{"entities": [[0, 9, "RISIKOPROFIL"]]},
),
(
"Core/Value Add",
{"entities": [[0, 14, "RISIKOPROFIL"]]},
),
(
"Core+/Value Add",
{"entities": [[0, 15, "RISIKOPROFIL"]]},
),
(
"Core/Core+/Value Add",
{"entities": [[0, 20, "RISIKOPROFIL"]]},
),
(
"The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
{"entities": [[82, 92, "RISIKOPROFIL"]]},
),
(
"Risk level: Core/Core+",
{"entities": [[12, 22, "RISIKOPROFIL"]]},
),
(
"Different risk profile (core, core+, value-added)",
{"entities": [[24, 48, "RISIKOPROFIL"]]},
),
(
"Core/Core+ with OpCo premium",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Core /Core+ Assets, well-established = Key Gateway Cities in Europe le.g. hotels in the market with minor asset London, Paris, Amsterdam, Berlin] management initiatives",
{"entities": [[0, 11, "RISIKOPROFIL"]]},
),
(
"Risikoprofil: Core, Core +",
{"entities": [[14, 26, "RISIKOPROFIL"]]},
),
(
"Name des Fonds Name des Investmentmanagers Allgemeine Informationen Name des Ansprechpartners Telefonnummer des Ansprechpartners E-Mail des Ansprechpartners Art des Anlagevehikels Struktur des Anlagevehikels Sitz des Anlagevehikels Struktur des Antagevehikels vom Manager festgelegter Stil Rechtsform Jahr des ersten Closings Laufzeit Geplantes Jahr der Auflösung Ziel-Netto-IRR / Gesamtrendite* Zielvolumen des Anlagevehikels Ziel-LTY Aktueller LTV Ziirraiaein Maximaler LTV Zielregionfen)/Jand Zielsektoren Zielanlagestrategie INREV Fonds Offen Deutschland Core, Core + Offener Immobilien-Spezialfonds 2022 10 - 12 Jahre 2032 - 2034 7,50%+ 250 Mio. € 20% 0% 20% Führende Metropolregionen Deutschlands und ausgewählte Standorte >50T Einw. Wohnimmobilien Wertstabile Wohnimmobilien (mit Bestandsentwicklungen)",
{"entities": [[560, 572, "RISIKOPROFIL"]]},
),
(
"Core/Core+ strategy, with tactical exposure to development projects aiming at enhancing the quality of the portfolio over time",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Strategie - Übersicht Risikoprofil Core+ Halten-Strategie Kaufen — Halten (langfristig) — Exit 1. Nachvermietungsstrategie Anlagestrategien 2. Standortaufwertungsstrategie 3. Strategie der Aufwertung der Immobilien Niederlande (max. 35 %) Länderallokation Frankreich (max. 35 %) (in % vom Zielvolumen) Skandinavien (Schweden, Dänemark) (max. 35 %) Deutschland (<= 10 %)",
{"entities": [[35, 40, "RISIKOPROFIL"]]},
),
(
"Core and Core+",
{"entities": [[0, 14, "RISIKOPROFIL"]]},
),
(
"core, core+, value-added",
{"entities": [[0, 24, "RISIKOPROFIL"]]},
),
(
"Manage to Core: max 20%",
{"entities": [[10, 14, "RISIKOPROFIL"]]},
),
(
"Benefits of the core/ core+ segment",
{"entities": [[16, 27, "RISIKOPROFIL"]]},
),
(
"Drawbacks of the core/ core+ segment",
{"entities": [[17, 28, "RISIKOPROFIL"]]},
),
(
"Why a Core / Core + investment program?",
{"entities": [[6, 19, "RISIKOPROFIL"]]},
),
(
"Different risk profile (core, core+, value-added)",
{"entities": [[24, 48, "RISIKOPROFIL"]]},
),
(
"INK MGallery Hotel Area: Amsterdam Core Tenant: Closed in 2018",
{"entities": [[35, 39, "RISIKOPROFIL"]]},
),
(
"A strategy targeting high quality Core and Core+ buildings, with defined SRI objectives, in order to extract value through an active asset management.",
{"entities": [[34, 48, "RISIKOPROFIL"]]},
),
(
"Navigate the diversity of the Core/Core+ investment opportunities in European Prime Cities",
{"entities": [[30, 40, "RISIKOPROFIL"]]},
),
(
"GEDis an open-ended Lux-based fund providing an attractive core/core+ real estate exposure, leveraging GRRE expertise in European RE markets. It offers diversification in terms of pan-European geographies and sectors: Offices, Retail and Hotels.",
{"entities": [[59, 69, "RISIKOPROFIL"]]},
),
(
"Core assets leave less room for active asset management value creation",
{"entities": [[0, 4, "RISIKOPROFIL"]]},
),
(
"capital preservation is defined here as a characteristic of core/core+ investments. There is no guarantee of capital.",
{"entities": [[60, 70, "RISIKOPROFIL"]]},
),
(
"Country / city BELGIUM Brussels BELGIUM Brussels SPAIN Madrid FRANCE Levallois FRANCE Paris 14 BELGIUM Brussels NETHERLANDS Rotterdam NETHERLANDS Rotterdam Sector Offices Offices Offices Offices Offices Offices Offices Logistics Risk Core",
{"entities": [[234, 238, "RISIKOPROFIL"]]},
),
(
"GERD(a balanced pan-European open ended retail fund — under the form of a French collective undertaking for Real Estate investments “OPCI”) is the flagship ofQin France and combines RE and listed assets (respective targets of 60% and 40%) with max. 40% leverage. The RE portfolio of the fund is a good illustration Of expertise in European core/core+ investments.",
{"entities": [[340, 350, "RISIKOPROFIL"]]},
),
(
"Prime office assets in Prime markets are very pricey unless rent reversion is real. Risk premium remains attractive on a leveraged basis. Manage to core or build to core can make sense as a LT investor in main cities. Residential is also attractive",
{"entities": [[148, 152, "RISIKOPROFIL"]]},
),
(
"Paris region is a deep and liquid market. Rents have some potential to improve. Considering current low yield and fierce competition, office right outside CBD for Core + assets can be considered. Manage to core strategies could make sense.",
{"entities": [[163, 169, "RISIKOPROFIL"]]},
),
(
"Lisbon is a small market but it experienced a rapid economic recovery in recent years and is interesting for Core Offices, quality Retail assetor Hotel walls with top operators. Limited liquidity of this market means investment must be small",
{"entities": [[109, 113, "RISIKOPROFIL"]]},
),
(
"4,0 %",
{"entities": [[0, 5, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Prognostizierte jährliche Ausschüttung von 4,0%",
{"entities": [[44, 48, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"20% über einer @ Ausschüttungsrendite von 4,0%",
{"entities": [[44, 48, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Prognostizierte Ausschüttungsrandite* Mindestanlage Mitgliedschaft Im Anlagesusschuss Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt.) Parformanceabhängige Vergütung Einmalige Strukturierungsgebühr Laufzeit / Investtionszeltraum Ausschüttungsintervalle Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Artikel 8 Wohnimmobilien Deutschland Aktive Bestandsentwicklung Offener Spezial-AlF mit festen Anlagebedingungen rd. 200 Mio. € / max. 20% rd. 250 Mio. € 7,5 % (nach Kosten & Gebühren, vor Steuern) 8 4,0 % {nach Kosten & Gebühren, var Steuern}",
{"entities": [[570, 575, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"5,00-5,25 % Ausschüttungsrendite",
{"entities": [[0, 11, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Zielrendite 5,00-5,25 % Ausschüttungsrendite",
{"entities": [[12, 23, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,9% 5,3%",
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,9% 5,3%",
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschittungsrendite 3,8% 5,7%",
{"entities": [[20, 24, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschittungsrendite 3,8% 5,7%",
{"entities": [[25, 29, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,5% 4,6%",
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 4,5% 4,6%",
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 5,0% 4,7%",
{"entities": [[26, 30, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite 5,0% 4,7%",
{"entities": [[21, 25, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite “eons a Nuremberg aha 5,0 % 4,8 %",
{"entities": [[43, 48, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Auschüttungsrendite “eons a Nuremberg aha 5,0 % 4,8 %",
{"entities": [[49, 54, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"3-4% dividend yield",
{"entities": [[0, 4, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Zielmärkte Klassifizierung SFDR Invastitionsfokus Rendite- / Risikoprofil Rechtsform Eigenkapital /FK Quote Investftionsvolumen Prognostizierte Gesamtrendite {IRR)* Prognostizierte Ausschüttungsrandite* Mindestanlage Mitgliedschaft Im Anlagesusschuss Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt.) Parformanceabhängige Vergütung Einmalige Strukturierungsgebühr Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Artikel 8 Wohnimmobilien Deutschland Aktive Bestandsentwicklung Offener Spezial-AlF mit festen Anlagebedingungen rd. 200 Mio. € / max. 20% rd. 250 Mio. € 7,5 % (nach Kosten & Gebühren, vor Steuern) 8 4,0 % {nach Kosten & Gebühren, var Steuern} 5Mio.€ Ab 10 Mio. € 1,40 % / 0,80 % /2,12% / 4,91 % Laufzeit / Investtionszeltraum Ausschüttungsintervalle 20 % über einer @ Ausschüttungsrendite von 4,0 % 0,1% der bis zum 31.12.2023 erfolgten Kapitalzusagen (max. 200.000 &) 10 bis 12 Jahre / bis zu 24 Monate angestrebt Mindestens jährlich",
{"entities": [[945, 960, "LAUFZEIT"]]},
),
(
"Laufzeit / Investtionszeltraum,10 bis 12 Jahre / bis zu 24 Monate angestrebt Ausschüttungsintervalle,Mindestens jährlich",
{"entities": [[31, 46, "LAUFZEIT"]]},
),
(
"10-12 Jahre Laufzeit bei einem LTV von bis zu 20%",
{"entities": [[0, 11, "LAUFZEIT"]]},
),
(
"vom Manager festgelegter Stil Rechtsform Jahr des ersten Closings Laufzeit Geplantes Jahr der Auflösung Ziel-Netto-IRR / Gesamtrendite* Zielvolumen des Anlagevehikels Ziel-LTYAktueller LTV Zielsektoren Zielanlagestrategie Fonds Offen Deutschland Core, Core + Offener Immobilien-Spezialfonds 2022 10 - 12 Jahre",
{"entities": [[297, 310, "LAUFZEIT"], [247, 259, "RISIKOPROFIL"]]},
),
(
"Allgemeine Annahmen Ankaufsphase Haltedauer Zielobjektgröße Finanzierung Investitions-annahmen Zielrendite 24 Monate Investmentzeitraum 10 Jahre (+) EUR 20-75 Mio. Keine externe Finanzierung zum Auftakt (ausschließlich Darlehen der Anteilseigner). Die Finanzierung wird nach der Ankaufsphase und Stabilisierung der Zinssätze neu geprüft. Angestrebter LTV zwischen 25-40 % Investitionen für Renovierungen und ESG- Verbesserungen werden für jedes Objekt einzeln festgelegt. 5,00-5,25 % Ausschüttungsrendites",
{"entities": [[136, 148, "LAUFZEIT"], [472, 483, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Zielrendite 5,00-5,25 % Ausschüttungsrendite 1) Ankauf von Objekten an Tag eins mit 100% Eigenkapital. Die Strategie unterstellt die Aufnahme von Fremdkapital, sobald sich die Zins- und Finanzierungskonditionen nachhaltig stabilisieren. Strategie - Übersicht Risikoprofil Core+",
{"entities": [[12, 23, "AUSSCHÜTTUNGSRENDITE"], [272, 277, "RISIKOPROFIL"]]},
),
(
"Vehicle lifetime / investment period Open-ended fund",
{"entities": [[37, 52, "LAUFZEIT"]]},
),
(
"Vehicle / domicile Alternative Investment Fund / Luxembourg (e.g. SCSp SICAV-RAIF) Investment strategy eturn pro Real Estate (PropCo + OpCo) Investing in upscale hotels with long-term management contracts in major European destinations Core/Core+ with OpCo premium Management Agreements solely with financially strong and experienced partners/ global brands Cash flow-oriented Cash-flow pattern Target equity /AuM € 400m equity / € 800m AuM (50% Loan-to-Value) Vehicle lifetime / investment period Open-ended fund",
{"entities": [[498, 513, "LAUFZEIT"], [236, 245, "RISIKOPROFIL"]]},
),
(
"Vehicle type (Lux-RAIF) (net of fees) IRR6.5% ACCOR Vehicle structure Open-ended Targetvehiclesize € 400m (equity) Manager-defined Core/Core+ with | style OpCo Premium darge CLV. 50% Pt H | LTO N WORLDWIDE Year of first closing 2020 Target no. ofinvestors 1-5 Fund life (yrs} Open-ended Min-commitmentper —¢ 400m",
{"entities": [[131, 141, "RISIKOPROFIL"], [70, 80, "LAUFZEIT"]]},
),
(
"Fund term: Open-ended",
{"entities": [[11, 21, "LAUFZEIT"]]},
),
(
"Abdeckung der Risiko-Rendite-Bandbreite (Core, Core+, Value-Add)",
{"entities": [[41, 63, "RISIKOPROFIL"]]},
),
(
"5,1% - 8,5% IRR!",
{"entities": [[0, 11, "RENDITE"]]},
),
(
"Retailinvestitionsvolumen nach Ländern (2024) Vereinigtes Königreich, 26,4% Deutschland, 19,0% Andere, 19,7% Italien, 8,2% Irland, 3,3% N | Frankreich, Spanien, 8,1%",
{"entities": [[46, 75, "LÄNDERALLOKATION"], [76, 94, "LÄNDERALLOKATION"], [95, 108, "LÄNDERALLOKATION"], [109, 122, "LÄNDERALLOKATION"], [123, 135, "LÄNDERALLOKATION"]]},
),
(
"Erwartete IRR 5 (je nach Objekt- A(E) 6.00% - 8,00%",
{"entities": [[39, 52, "RENDITE"]]},
),
(
"Zielmarkts Deutsche Metropolregianen und umliegende Regionen mit Städten >50T Einwohner Klassifizierung SFDR Artikel 8 Invastitionsfokus Wohnimmobilien Deutschland Rendite- / Risikoprofil Aktive Bestandsentwicklung Rechtsform Offener Spezial-AlF mit festen Anlagebedingungen Eigenkapital /FK Quote rd. 200 Mio. € / max. 20% Investftionsvolumen rd. 250 Mio. € Prognostiderte Gesamtrendite {IRR)* 7,5 % (nach Kosten & Gebühren, vor Steuern) Prognostizierte Ausschüttungsrandite* @ 4,0 % {nach Kosten & Gebühren, var Steuern} Mindestanlage 5Mio.€ Mitgliedschaft Im Anlagesusschuss Ab 10 Mio. € Ankaufs- / Verkaufs- / Verkaufs(Teflimmobilfe)- / Baumanagementgebahr (inkl. USt) 1,40 %/080%/212%/491% Parformanceabhängige Vergütung 20 % über einer ® Ausschüttungsrendite von 4,0% Einmalige Strukturierungsgebühr 0,1% der bis zum 31.12.2023 erfolgten Kapitalzusagen (max. 200.000 €) Laufzelt / Investtonszeltraum 10 bis 12 Jahre / bis zu 24 Monate angestrebt Ausschüttungsintervalle Mindestens jährlich",
{"entities": [[396, 401, "RENDITE"], [482, 487, "AUSSCHÜTTUNGSRENDITE"], [914, 929, "LAUFZEIT"]]},
),
(
"= Prognostizierte jährliche Ausschüttung von @ 4,0%* = Prognostizierte Gesamtrendite (IRR) von 7,5%*",
{"entities": [[48, 52, "AUSSCHÜTTUNGSRENDITE"], [96, 100, "RENDITE"]]},
),
(
"Prognose: 7,5%+ IRR auf Fondsebene",
{"entities": [[10, 14, "RENDITE"]]},
),
(
"= Prognostizierte jährliche Ausschüttung* von 84,0% = Prognostizierte Gesamtrendite (IRR}* von 7,5%",
{"entities": [[96, 100, "RENDITE"], [49, 53, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"= Lagefokussierung: Metropolregionen Deutschlands = Finanzierung: max. 20% LTV = Risikoprofil: Core, Core +",
{"entities": [[95, 107, "RISIKOPROFIL"]]},
),
(
"Performance-Fee: 20% über einer @ Ausschüttungsrendite von 4,0%",
{"entities": [[61, 65, "AUSSCHÜTTUNGSRENDITE"]]},
),
(
"Fondstyp Offener Spezial-AIF nach KAGB mit festen Anlagebedingungen ESG-Klassifizierung Fonds gemäß Artikel 8 EU-Offenlegungsverordnung KVG IntReal GmbH, Hamburg Anlagestrategie Aufbau eines Objektportfolios aus Ärztehäusern, die langfristig vermietet sind Ärztehäuser, Laborimmobilien, im Verbund mit Ärztehäusern auch ambulant Zielobjekte betreute Wohngemeinschaften; Mietanteil Medizin und medizinnahe Dienstleistungen/Handel > 65 % (Objektebene) WALT >5 Jahre bei Ankauf Objektbaujahre Ab 2000 Anlagegrenzen Einzelinvestment 8-30 Mio. EUR Anzahl Objekte 10-20 Deutschland bundesweit; jeweiliges Einzugsgebiet > 25.000 Einwohner mit Regionen stabiler Bevölkerungsprognose Risikoprofil Core / Core +",
{"entities": [[689, 702, "RISIKOPROFIL"]]},
),
(
"Fondsvolumen 300 Mio. EUR Zielrendite (IRR) > 6,0 % p. a. Ausschuttung >5,0 % p. a. Ankaufszeitraum 2024-2026 Laufzeit 31.12.2036 Mindestanlage 10 Mio. EUR Anlageausschuss Ja, entscheidet u. a. über Objekterwerb (Mitglied kann ab 20 Mio. EUR gestellt werden) Gebührenstruktur Marktüblich (auf Anfrage) Projektentwicklungen keine Forward-Deals Möglich, maximal 18 Monate Vorlauf; keine Projektentwicklungsrisiken beim Fonds Erbbaurechte Möglich, sofern Laufzeit > 60 Jahre und angemessene Entschädigung bei Ablauf und Heimfall Status Objektpipeline vorhanden: siehe Folie 16 ff.",
{"entities": [[44, 57, "RENDITE"], [71, 83, "AUSSCHÜTTUNGSRENDITE"], [120, 130, "LAUFZEIT"]]},
),
(
"Niederlande (max. 35 %) Länderallokation Frankreich (max. 35 %) (in % vom Zielvolumen) Skandinavien (Schweden, Dänemark) (max. 35 %) Deutschland (<= 10 %)",
{"entities": [[0, 23, "LÄNDERALLOKATION"], [41, 63, "LÄNDERALLOKATION"], [87, 132, "LÄNDERALLOKATION"], [133, 154, "LÄNDERALLOKATION"]]},
),
(
"Führender Immobilien-Investmentmanager in den Nordics für globale ll institutionelle Investoren in Value Add und Core Strategien",
{"entities": [[101, 119, "RISIKOPROFIL"]]},
),
(
"Core und Core+ Fonds",
{"entities": [[0, 14, "RISIKOPROFIL"]]},
),
(
"Risikoprofil Core / Core+",
{"entities": [[13, 25, "RISIKOPROFIL"]]},
),
(
"Durchschnittlich geplante jährliche Ausschüttung von 4,5-5,5% auf das investierte Eigenkapital an die Anleger Geplante Gesamtrendite von 5-6% (IRR) auf das eingezahlte Eigenkapital",
{"entities": [[54, 62, "AUSSCHÜTTUNGSRENDITE"], [138, 142, "RENDITE"]]},
),
(
"Geografische Zielallokation nach Investitionsphase des Fonds: 1) Schweden 20-60% Allokation Länder 2) Finnland 20-60% 3) Norwegen 10-40% 4) Dänemark 10-40%",
{"entities": [[65, 80, "LÄNDERALLOKATION"], [102, 117, "LÄNDERALLOKATION"], [121, 136, "LÄNDERALLOKATION"], [140, 155, "LÄNDERALLOKATION"]]},
),
(
"Deutsches Spezial-Sondervermögen mit festen Anlagebedingungen ($284 KAGB) Immobilien- oder Infrastrukturquote (nach Solvency II) Core / Core+ Euro Hauptstadtregionen und andere Großstädte in den Nordics €500 Mio. 4,5-5,5% 15 Jahre; Fonds hat unbegrenzte Laufzeit; Investmentphase 4 Jahre Maximaler Fremdkapitalanteil 50% (LTV-Ziel bei Ankauf), Langfristiges LTV-Ziel auf Fondsebene ist 45% 0,625% p. a. des Bruttofondsvermögens Zeichnungen ab € 30 Mio. - 0,03 % Rabatt Zeichnungen ab € 50 Mio. - zusatzl. 0,03 % Rabatt 1,1% des Verkehrswertes 0,6% der Bruttoverkaufswert 10% wenn Hurdle Rate 5,0 % p. a. (IRR netto) überschritten wird (nach 15 Jahren berechnet) Ja",
{"entities": [[129, 141, "RISIKOPROFIL"], [213, 221, "ZIELRENDITE"], [242, 262, "LAUFZEIT"]]},
),
(
"Standort Helsinki, Finnland Sektor Bildungswesen, Schule& Kindertagesstätte Vermietbare Fläche 3.321 m? Leerstand bei Ankauf 0% / 0% Ankaufspreis+ Investitionen €21,4 Mio. + €0,2 Mio Eigenkapital €21,6 Mio. Ankaufs- / Stabilisierungs- / Exitrendite 5,0%/ 5,5%/ 5,0% NOI zum Ankaufszeitpunkt / Exit-NOI €1.1m/ €1.2m Zielrenditen (netto für LPs) 5,4% IRR/ 1.5x EM / DY 4,3% Ankauf / Exit Dezember 2023/ Dezember 2033",
{"entities": [[345, 349, "ZIELRENDITE"]]},
),
(
"Evergreen/offene Fondsstrukturenv Core / Core+ Strategien",
{"entities": [[34, 46, "RISIKOPROFIL"]]},
),
(
"BEE Henderson German 2012 Logistik Core/D/Art. 8 € 336 Mio. 12 (voll investiert) 13,0 % p.a.",
{"entities": [[35, 39, "RISIKOPROFIL"], [81, 87, "RENDITE"]]},
),
(
"ICF German Logistics 2014 Logistik Core/D/Art. 8 € 400 Mio. 16 (voll investiert) 12,0 % p.a.",
{"entities": [[35, 39, "RISIKOPROFIL"], [81, 87, "RENDITE"]]},
),
(
"Individualmandat 2015 Logistik Core / D+AU/ ArTt. 6 € 200 Mio. 8 (realisiert) 8,0 % p.a.",
{"entities": [[31, 35, "RISIKOPROFIL"], [78, 83, "RENDITE"]]},
),
(
"European Logistics Partnership” 2017 Logistik Value-Add / Europ/a - € 1.000 Mio. 28 (realisiert) 20,0 % p.a.",
{"entities": [[46, 55, "RISIKOPROFIL"], [97, 103, "RENDITE"]]},
),
(
"European Core Logistics Fund (ECLF 1) 2021 Logistik Core / Euro/p Arat. 8 € 314 Mio. 12 (voll investiert) 7,50 % p.a.",
{"entities": [[9, 13, "RISIKOPROFIL"], [106, 112, "RENDITE"]]},
),
(
"P-Logistik Europa Fonds (ECLF 2) 2022 Logistik Core / Euro/p Arat. 8 € 150 Mio.? A (voll investiert) 6,5 % p.a.?",
{"entities": [[47, 51, "RISIKOPROFIL"], [101, 106, "RENDITE"]]},
),
(
"First Business Parks 2015 Light Industrial Value Add / D+AUT € 100 Mio. 6 (realisiert) 16,0 % p.a.",
{"entities": [[43, 52, "RISIKOPROFIL"], [87, 93, "RENDITE"]]},
),
(
"Unternehmensimmobilien Club 1 2016 Light Industrial Core+/D € 186 Mio. 9 (voll investiert) 13,0 % p.a.",
{"entities": [[91, 97, "RENDITE"]]},
),
(
"Unternehmensimmobilien Club 1 2016 Light Industrial Core+/D € 186 Mio. 9 (voll investiert) 13,0 % p.a.",
{"entities": [[52, 57, "RISIKOPROFIL"], [91, 97, "RENDITE"]]},
),
(
"Unternehmensimmobilien Club 2 2021 Light Industrial Core+/D € 262 Mio. 12 (voll investiert) 9,00 % p.a.",
{"entities": [[52, 57, "RISIKOPROFIL"], [92, 98, "RENDITE"]]},
),
(
"Individualmandat 2022 Light Industrial Value-Add / Nordics € 100 Mio. 5 (voll investiert) 18,0 % p.a.",
{"entities": [[39, 48, "RISIKOPROFIL"], [90, 96, "RENDITE"]]},
),
(
"EUROPEAN CORE LOGISTICS FUND 3",
{"entities": [[9, 13, "RISIKOPROFIL"]]},
),
(
"Core Investitionen",
{"entities": [[0, 4, "RISIKOPROFIL"]]},
),
(
"8 % IRR",
{"entities": [[0, 3, "RENDITE"]]},
),
(
"Rendite-Risiko-Profil Core ° Geographischer Fokus Kontinentaleuropaische Kernvolkswirtschaften nach Allokationsprofil * Sektoraler Fokus Logistikimmobilien nach Allokationsprofil Kapitalstruktur ° Eigenkapital € 250 Mio. ° Fremdkapital 50 % angestrebt, max. 60 % der Immobilienwerte (Objektebene) °e Mindestzeichnung € 10 Mio. Vehikelstruktur ° Rechtsform Immobilien-Spezial-AlF mit festen Anlagebedingungen nach 3 284 KAGB ° Klassifikation Artikel 8 Offenlegungsverordnung ¢ Anlagehorizont 10 Jahre mit Verlängerungsoption um 2 Jahre! ° Geplante Auflage 01 2025 Performanceziel? ° Ausschüttung 6,0 % p.a. (Durchschnitt 10 Jahre Haltedauer) ° Interner Zinsfuß (IRR) 8,0 % p.a. (10 Jahre Haltedauer, Target-IRR)",
{"entities": [[22, 26, "RISIKOPROFIL"], [596, 601, "AUSSCHÜTTUNGSRENDITE"], [667, 672, "RENDITE"]]},
),
(
"Core/Core+, mit Cash-Flow-Stabilität",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Zielausschüttung: min. 5,10%",
{"entities": [[24, 29, "ZIELAUSSCHÜTTUNG"]]},
),
(
"Zielrendite (IRR): min. 5,50%",
{"entities": [[24, 29, "ZIELRENDITE"]]},
),
(
"Rewe & Lidl Maxhütte-Haidhof é ae: 6 s Bahnhof Ankermieter REWE & Lidl er WALT 20 und 17 Jahre Miete p.a. 1.127.916 € Kaufpreis 21,43 Mio. € Faktor 19,00 x LTV / Zins 80% / 4,0% Ausschüttung 5,7 % IRR 7,1%",
{"entities": [[193, 198, "AUSSCHÜTTUNGSRENDITE"], [203, 207, "ZIELRENDITE"]]},
),
(
"Real Estate Prime Europe Access the Core of European Prime Cities with a green SRI fund including a genuine low carbon commitment",
{"entities": [[36, 40, "RISIKOPROFIL"]]},
),
(
"(FR, UK, DE, BE, NL, LU, Nordics, Allocation SP, IT, CH)",
{"entities": [[1, 32, "LÄNDERALLOKATION"], [45, 55, "LÄNDERALLOKATION"]]},
),
(
"IRR: 6% - 7%",
{"entities": [[5, 12, "RENDITE"]]},
),
(
"Europe | Germany 67 Value Add",
{"entities": [[9, 16, "LÄNDERALLOKATION"], [20, 29, "RISIKOPROFIL"]]},
),
(
"Germany, Norway 336 Core Plus",
{"entities": [[0, 7, "LÄNDERALLOKATION"], [20, 29, "RISIKOPROFIL"]]},
),
(
"UK",
{"entities": [[0, 2, "LÄNDERALLOKATION"]]},
),
(
"NORWAY",
{"entities": [[0, 6, "LÄNDERALLOKATION"]]},
),
(
"9.8% IRR",
{"entities": [[0, 4, "RENDITE"]]},
),
(
"Investment volume down 52% to €2.3 billion, with 4,000 100 14% value-add and core-plus increasing YoY",
{"entities": [[63, 86, "RISIKOPROFIL"]]},
),
(
"Geared Gross IRR seeking a range of 16-18% per annum",
{"entities": [[37, 43, "RENDITE"]]},
),
(
"Open-ended fund 24 months, incl. rolling reinvestment Sale of individual assets with respective management contracts or geared leases IRR: >6.5% | CoC: >5.0%",
{"entities": [[0, 10, "LAUFZEIT"], [139, 144, "RENDITE"]]},
),
(
"Our investment strategy focuses on investing in upscale hotels in European prime locations, including DACH, Italy, Spain, Portugal, France, UK, Denmark, Benelux,and Poland.",
{"entities": [[102, 171, "LÄNDERALLOKATION"]]},
),
(
"Core+ assets with value-add potential, Emerging Gateway Cities Helsinki] Core+ with Value well-mitigated risk and great upside Potential potential through asset improvement or = Max. 20% UK & Ireland {no contract renegotiation currency risk hedging], 80% tinental E > IRR target of 6-9%",
{"entities": [[0, 5, "RISIKOPROFIL"], [282, 286, "RENDITE"]]},
),
(
"10% net IRR since inception in 2018?",
{"entities": [[0, 3, "RENDITE"]]},
),
(
"Eurozone: Benelux, France and Germany",
{"entities": [[10, 37, "LÄNDERALLOKATION"]]},
),
(
"Open-ended, with quarterly liquidity (redemption rights, dual pricing)",
{"entities": [[0, 10, "LAUFZEIT"]]},
),
(
"Class A & B (Institutional): 0.93% on NAV; Class D (Wholesale): 1.80% on NAV; Class P (Wholesale): 1.25% on NAV",
{"entities": [[29, 34, "MANAGMENTGEBÜHREN"], [64, 69, "MANAGMENTGEBÜHREN"], [99, 104, "MANAGMENTGEBÜHREN"]]},
),
(
"Risk profile: favour core > © at least and core+ assets with a targeted N 2 n allocation to value add assets to enhance returns",
{"entities": [[21, 25, "RISIKOPROFIL"], [43, 48, "RISIKOPROFIL"]]},
),
(
"The Netherlands (38 assets) = Germany (9 assets) 10 largest Country assets split France (8 assets)",
{"entities": [[0, 15, "LÄNDERALLOKATION"], [30, 37, "LÄNDERALLOKATION"], [81, 87, "LÄNDERALLOKATION"]]},
),
(
"Expected IRR 10.9%",
{"entities": [[13, 18, "ZIELRENDITE"]]},
),
(
"Structure Open-end, perpetual life, Luxembourg domiciled Initial Target Size* €2 billion 6-8% total return,",
{"entities": [[10, 18, "LAUFZEIT"], [89, 93, "RENDITE"]]},
),
(
"Geographic Focus: UK, Ireland, Iberia, Nordics, Netherlands, Germany, France, Italy",
{"entities": [[18, 83, "LÄNDERALLOKATION"]]},
),
(
"IRR of 13-14%",
{"entities": [[7, 13, "RENDITE"]]},
),
(
"Value-add",
{"entities": [[0, 9, "RISIKOPROFIL"]]},
),
(
"Geographic allocation NORDICS UNITED KINGDOM GERMANY FRANCE PORTUGAL BENELUX",
{"entities": [[22, 76, "LÄNDERALLOKATION"]]},
),
(
"Strong track record delivering a 17% net IRR, 1.7x net multiple across all divested assets (both discretionary and non-discretionary mandates)",
{"entities": [[33, 36, "RENDITE"]]},
),
(
"Targeting a 7-8% net annual return and a 3-4% dividend yield, reflecting a target LTV of 35% (capped at 37.5%)",
{"entities": [[12, 16, "RENDITE"]]},
),
(
"Sweden Norway Denmark Finland",
{"entities": [[0, 29, "LÄNDERALLOKATION"]]},
),
(
"Logistics Residential Office Other",
{"entities": [[0, 34, "SEKTORENALLOKATION"]]},
),
(
"Fund Term Open-ended with an initial 24-month lock-in for new investors",
{"entities": [[10, 20, "LAUFZEIT"]]},
),
(
"Management fee of 85 bps on NAV.",
{"entities": [[18, 24, "MANAGMENTGEBÜHREN"]]},
),
(
"Core/Core+ strategy, with tactical exposure to development projects aiming at enhancing the quality of the portfolio over time",
{"entities": [[0, 10, "RISIKOPROFIL"]]},
),
(
"Fund term: Open-ended",
{"entities": [[11, 21, "LAUFZEIT"]]},
),
(
"Return targets: The fund targets a net internal rate of return (IRR) of 8% and a net annual income yield of 5% with planned quarterly distributions.",
{"entities": [[72, 74, "RENDITE"]]},
),
(
"Geographic scope: The fund has a broad mandate to invest in commercial and residential real estate across Sweden, Denmark, Finland, and Norway. 50% LTV Asset selection: Heirs to acquire high-quality, income-generating properties in major Nordic cities and enhance their value through active asset management. Portfolio construction: The goal is to build diversified portfolios that are appealing to core buyers upon exit.",
{"entities": [[106, 142, "LÄNDERALLOKATION"]]},
),
(
"Experience: Since 2012, | | has demonstrated its capability to build diversified and resilient portfolios for its core-plus funds. German Real Estate Quota advantage . Local expertise: extensive local relationships and proprietary deal flow in key Nordic markets provide a strategic advantage.",
{"entities": [[114, 123, "RISIKOPROFIL"]]},
),
(
"Target returns: 8% net IRR with 5% net annual income yield! * Geographic focus: Sweden, Denmark, Norway and Finland « Target leverage: 50% LTV (excluding short-term borrowing) « Sector exposure: office, logistics, public properties, retail (focused on grocery anchored and necessity driven retail) and residentials « Investment focus: high quality properties,",
{"entities": [[16, 18, "RENDITE"], [80, 115, "LÄNDERALLOKATION"], [195, 239, "SEKTORENALLOKATION"]]},
),
(
"The Fund 2 xemoours common limited partnership (SCS) (SICAV-RAIF) Investment Objective To pursue investments in commercial and residential properties throughout the Nordic Region Fund Target Size €300 million (equity) Return Targets Target net IRR of 8%, target net annual income yield of 5%",
{"entities": [[251, 253, "RENDITE"]]},
)
]

View File

@ -1,40 +0,0 @@
import os
from pathlib import Path
import spacy
from spacy.cli.train import train
from spacy.tokens import DocBin
from tqdm import tqdm
from training_data import TRAINING_DATA
nlp = spacy.blank("de")
# create a DocBin object
db = DocBin()
for text, annot in tqdm(TRAINING_DATA):
doc = nlp.make_doc(text)
ents = []
# add character indexes
for start, end, label in annot["entities"]:
span = doc.char_span(start, end, label=label, alignment_mode="contract")
if span is None:
print(f"Skipping entity: |{text[start:end]}| Start: {start}, End: {end}, Label: {label}")
else:
ents.append(span)
# label the text with the ents
doc.ents = ents
db.add(doc)
# save the DocBin object
os.makedirs("./data", exist_ok=True)
db.to_disk("./data/train.spacy")
config_path = Path("config.cfg")
output_path = Path("output")
print("Starte Training...")
train(config_path, output_path)

View File

@ -0,0 +1 @@
{"running": false}

View File

@ -40,7 +40,9 @@ def send_to_coordinator_service(processed_data, request_id):
def process_data_async(request_id, spacy_data, exxeta_data):
try:
requests.post(COORDINATOR_URL + "/api/progress", json={"id": request_id, "progress": 95})
requests.post(
COORDINATOR_URL + "/api/progress", json={"id": request_id, "progress": 95}
)
print(f"Start asynchronous processing for PitchBook: {request_id}")
# Perform merge
@ -96,7 +98,6 @@ def validate():
# If both datasets are present, start asynchronous processing
if spacy_data is not None and exxeta_data is not None:
# Start asynchronous processing in a separate thread
processing_thread = threading.Thread(
target=process_data_async,

View File

@ -27,7 +27,6 @@ def merge_entities(spacy_data, exxeta_data):
and s_entity_norm == e_entity_norm
and s_page == e_page
):
merged.append(
{
"label": s["label"],

View File

@ -5,6 +5,8 @@ import os
# SETTINGS = [{"id": "Rendite", "type": "number"}]
COORDINATOR_URL = os.getenv("COORDINATOR_URL", "http://localhost:5000")
def validate_entities(entities):
try:
response = requests.get(COORDINATOR_URL + "/api/kpi_setting/")
@ -42,7 +44,6 @@ def validate_entities(entities):
result.extend(item[1])
continue
# Filter not validated, if there are valid values
validated = False
for entity in item[1]:
@ -61,11 +62,11 @@ def validate_entities(entities):
def validate_number(entity_list, settings):
filtered_kpi = {}
for label, entity_list in entity_list.items():
setting = next((s for s in settings if s["name"].upper() == label), None)
if setting and setting["type"] == "number":
filtered_entities = [
entity for entity in entity_list
entity
for entity in entity_list
if is_valid_number(str(entity["entity"]))
]
for entity in entity_list:
@ -80,8 +81,12 @@ def validate_number(entity_list, settings):
def is_valid_number(number):
pattern = r'^[0-9\-\s%,.€]+$'
return any(char.isdigit() for char in number) and not re.search(r'\d+\s\d+', number) and re.fullmatch(pattern, number)
pattern = r"^[0-9\-\s%,.€]+$"
return (
any(char.isdigit() for char in number)
and not re.search(r"\d+\s\d+", number)
and re.fullmatch(pattern, number)
)
def delete_exxeta_unknown(entity_list):
@ -89,11 +94,16 @@ def delete_exxeta_unknown(entity_list):
for label, entity_list in entity_list.items():
# Filter out entities with "nichtangegeben" or "n/a" (case-insensitive and stripped)
filtered_entities = [
entity for entity in entity_list
if str(entity["entity"]).lower().replace(" ", "") not in {"nichtangegeben", "n/a"}
entity
for entity in entity_list
if str(entity["entity"]).lower().replace(" ", "")
not in {"nichtangegeben", "n/a"}
]
for entity in entity_list:
if str(entity["entity"]).lower().replace(" ", "") in {"nichtangegeben", "n/a"}:
if str(entity["entity"]).lower().replace(" ", "") in {
"nichtangegeben",
"n/a",
}:
print(f"filtered out: {entity}")
if filtered_entities: # Only add the label if there are entities left
filtered_kpi[label] = filtered_entities
@ -115,6 +125,7 @@ def delete_duplicate_entities(entity_list):
unique_entities[label] = filtered_entities
return unique_entities
if __name__ == "__main__":
entities = [
# {"label": "PERSON", "entity": "John Doe", "status": "validated"},

View File

@ -58,6 +58,8 @@ services:
- VALIDATE_SERVICE_URL=http://validate:5000/validate
ports:
- 5052:5052
volumes:
- ./backend/spacy-service/spacy_training:/app/spacy_training
exxeta:
build:

View File

@ -4,7 +4,9 @@ WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
#RUN bun install --frozen-lockfile
RUN bun install
1924466 marked this conversation as resolved

Wieso hast du das geändert? Also scheint keinen Unterschied zu machen, aber interessiert mich, ob das besser ist?

Wieso hast du das geändert? Also scheint keinen Unterschied zu machen, aber interessiert mich, ob das besser ist?
COPY . .

6335
project/frontend/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@
"@biomejs/biome": "1.9.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/file-saver": "^2.0.7",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",

View File

@ -6,28 +6,32 @@ import type { Kennzahl } from "../types/kpi";
import { getDisplayType } from "../types/kpi";
import { fetchKennzahlen as fetchK } from "../util/api";
import { API_HOST } from "../util/api";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
type ConfigTableProps = {
export type ConfigTableProps = {
from?: string;
trainingRunning?: boolean;
};
export function ConfigTable({ from }: ConfigTableProps) {
export function ConfigTable({ from, trainingRunning }: ConfigTableProps) {
const navigate = useNavigate();
const [kennzahlen, setKennzahlen] = useState<Kennzahl[]>([]);
const [draggedItem, setDraggedItem] = useState<Kennzahl | null>(null);
const [isUpdatingPositions, setIsUpdatingPositions] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchKennzahlen = async () => {
while (true) {
try {
console.log("Fetching kennzahlen from API...");
const data = await fetchK();
console.log("Fetched kennzahlen:", data);
const sortedData = data.sort(
(a: Kennzahl, b: Kennzahl) => a.position - b.position,
);
const sortedData = data.sort((a, b) => a.position - b.position);
setKennzahlen(sortedData);
setLoading(false);
break;
@ -38,9 +42,20 @@ export function ConfigTable({ from }: ConfigTableProps) {
}
};
useEffect(() => {
fetchKennzahlen();
}, []);
useEffect(() => {
if (trainingRunning === false) {
console.log("[ConfigTable] Training beendet → Kennzahlen neu laden...");
fetchKennzahlen();
}
}, [trainingRunning]);
const handleToggleActive = async (id: number) => {
const kennzahl = kennzahlen.find((k) => k.id === id);
if (!kennzahl) return;
@ -326,15 +341,32 @@ export function ConfigTable({ from }: ConfigTableProps) {
padding: "12px",
fontSize: "14px",
color: "#333",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<span title={`Click to view details (ID: ${kennzahl.id})`}>
{kennzahl.name}
{kennzahl.mandatory && (
<span> *</span>
)}
{kennzahl.mandatory && <span> *</span>}
</span>
{kennzahl.is_trained === false && (
<Tooltip
title={
<>
<b>Diese Kennzahl ist nicht trainiert.</b><br />
Klicken Sie oben auf <i>"Neu trainieren"</i>, um das Training zu starten.
</>
}
arrow
placement="right"
>
<WarningAmberIcon sx={{ color: "#f57c00", fontSize: 20 }} />
</Tooltip>
)}
</td>
<td style={{ padding: "12px" }}>
<span
style={{

View File

@ -1,8 +1,13 @@
import { Box, Typography, Button, Paper, TextField, FormControlLabel,
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress } from "@mui/material";
import {
Box, Typography, Button, Paper, TextField, FormControlLabel,
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress
} from "@mui/material";
import { useState, useEffect } from "react";
import type { Kennzahl } from "../types/kpi";
import { typeDisplayMapping } from "../types/kpi";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
interface KPIFormProps {
mode: 'add' | 'edit';
@ -10,47 +15,138 @@ interface KPIFormProps {
onSave: (data: Partial<Kennzahl>) => Promise<void>;
onCancel: () => void;
loading?: boolean;
resetTrigger?: number;
}
const emptyKPI: Partial<Kennzahl> = {
name: '',
description: '',
mandatory: false,
type: 'string',
translation: '',
example: '',
active: true
active: true,
examples: [{ sentence: '', value: '' }],
};
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) {
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
const [isSaving, setIsSaving] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error' | 'info'>("success");
useEffect(() => {
if (mode === 'edit' && initialData) {
setFormData(initialData);
} else {
} else if (mode === 'add') {
setFormData(emptyKPI);
}
}, [mode, initialData]);
useEffect(() => {
if (mode === 'add') {
setFormData(emptyKPI);
}
}, [resetTrigger]);
const handleSave = async () => {
if (!formData.name?.trim()) {
alert('Name ist erforderlich');
setSnackbarMessage("Name ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
if (!formData.examples || formData.examples.length === 0) {
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
for (const ex of formData.examples) {
if (!ex.sentence?.trim() || !ex.value?.trim()) {
setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.');
setSnackbarSeverity("error");
setSnackbarOpen(true);
return;
}
}
setIsSaving(true);
try {
await onSave(formData);
} catch (error) {
console.error('Error saving KPI:', error);
const spacyEntries = generateSpacyEntries(formData);
// Für jeden einzelnen Beispielsatz:
for (const entry of spacyEntries) {
// im localStorage speichern (zum Debuggen oder Vorschau)
const stored = localStorage.getItem("spacyData");
const existingData = stored ? JSON.parse(stored) : [];
const updated = [...existingData, entry];
localStorage.setItem("spacyData", JSON.stringify(updated));
// POST Request an das Flask-Backend
const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(entry)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
}
console.log("SpaCy-Eintrag gespeichert:", data);
}
// Dann in die DB speichern
await onSave({
name: formData.name!,
mandatory: formData.mandatory ?? false,
type: formData.type || 'string',
position: formData.position ?? 0,
active: formData.active ?? true,
examples: formData.examples ?? [],
is_trained: false,
});
// Formular zurücksetzen:
setFormData(emptyKPI);
setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen.");
setSnackbarSeverity("success");
setSnackbarOpen(true);
} catch (e: any) {
// Prüfe auf 409-Fehler
if (e?.message?.includes("409") || e?.response?.status === 409) {
setSnackbarMessage("Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten.");
setSnackbarSeverity("info");
setSnackbarOpen(true);
} else {
setSnackbarMessage(e.message || "Fehler beim Speichern.");
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
console.error(e);
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
setFormData(emptyKPI);
onCancel();
};
@ -58,6 +154,24 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
setFormData(prev => ({ ...prev, [field]: value }));
};
const updateExample = (index: number, field: 'sentence' | 'value', value: string) => {
const newExamples = [...(formData.examples || [])];
newExamples[index][field] = value;
updateField('examples', newExamples);
};
const addExample = () => {
const newExamples = [...(formData.examples || []), { sentence: '', value: '' }];
updateField('examples', newExamples);
};
const removeExample = (index: number) => {
const newExamples = [...(formData.examples || [])];
newExamples.splice(index, 1);
updateField('examples', newExamples);
};
if (loading) {
return (
<Box
@ -76,6 +190,7 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
}
return (
<>
<Paper
elevation={2}
sx={{
@ -93,6 +208,7 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
<TextField
fullWidth
label="Name *"
placeholder="z.B. IRR"
value={formData.name || ''}
onChange={(e) => updateField('name', e.target.value)}
sx={{ mb: 2 }}
@ -104,39 +220,6 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beschreibung
</Typography>
<TextField
fullWidth
multiline
rows={3}
label="Beschreibung"
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
helperText="Beschreibung der Kennzahl"
/>
<Box mt={3}>
<FormControlLabel
control={
<Checkbox
checked={formData.mandatory || false}
onChange={(e) => updateField('mandatory', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Erforderlich"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Typography>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
@ -157,37 +240,6 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
</FormControl>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Synonyme & Übersetzungen
</Typography>
<TextField
fullWidth
label="Übersetzung"
value={formData.translation || ''}
onChange={(e) => updateField('translation', e.target.value)}
helperText="z.B. Englische Übersetzung der Kennzahl"
/>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispiele von Kennzahl
</Typography>
<TextField
fullWidth
multiline
rows={2}
label="Beispiel"
value={formData.example || ''}
onChange={(e) => updateField('example', e.target.value)}
helperText="Beispielwerte für diese Kennzahl"
/>
</Box>
{mode === 'add' && (
<>
@ -207,9 +259,81 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
Die Kennzahl ist aktiv und wird angezeigt
</Typography>
</Box>
<Box mt={3}>
<FormControlLabel
control={
<Checkbox
checked={formData.mandatory || false}
onChange={(e) => updateField('mandatory', e.target.checked)}
sx={{ color: '#383838' }}
/>
}
label="Erforderlich"
/>
<Typography variant="body2" color="text.secondary" ml={4}>
Die Kennzahl erlaubt keine leeren Werte
</Typography>
</Box>
</>
)}
<Divider sx={{ my: 3 }} />
{/* Hinweistext vor Beispielsätzen */}
<Box mb={2} p={2} sx={{ backgroundColor: '#fff8e1', border: '1px solid #ffe082', borderRadius: 2 }}>
<Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
Hinweis zur Trainingsqualität
</Typography>
<Typography variant="body2">
Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu erstellen je mehr, desto besser.
</Typography>
<Typography variant="body2" mt={1}>
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button <em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
</Typography>
<Typography variant="body2" mt={1}>
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
</Typography>
</Box>
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsätze
</Typography>
{(formData.examples || []).map((ex, idx) => (
<Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
<TextField
fullWidth
multiline
label={`Beispielsatz ${idx + 1}`}
placeholder="z.B. Die IRR beträgt 7,8 %"
value={ex.sentence}
onChange={(e) => updateExample(idx, 'sentence', e.target.value)}
required
sx={{ mb: 1 }}
/>
<TextField
fullWidth
label="Bezeichneter Wert im Satz"
placeholder="z.B. 7,8 %"
value={ex.value}
onChange={(e) => updateExample(idx, 'value', e.target.value)}
required
/>
{(formData.examples?.length || 0) > 1 && (
<Button onClick={() => removeExample(idx)} sx={{ mt: 1 }} color="error">
Entfernen
</Button>
)}
</Box>
))}
<Button variant="outlined" onClick={addExample}>
+ Beispielsatz hinzufügen
</Button>
</Box>
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
<Button
variant="outlined"
@ -243,5 +367,45 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
</Button>
</Box>
</Paper>
<Snackbar
open={snackbarOpen}
autoHideDuration={5000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => setSnackbarOpen(false)}
severity={snackbarSeverity}
sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<span>{snackbarMessage}</span>
<Button color="inherit" size="small" onClick={() => setSnackbarOpen(false)}>
OK
</Button>
</MuiAlert>
</Snackbar>
</>
);
}
function generateSpacyEntries(formData: Partial<Kennzahl>) {
const label = formData.name?.trim().toUpperCase() || "";
return (formData.examples || []).map(({ sentence, value }) => {
const trimmedValue = value.trim();
const start = sentence.indexOf(trimmedValue);
if (start === -1) {
throw new Error(`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`);
}
return {
text: sentence,
entities: [[start, start + trimmedValue.length, label]]
};
});
}

View File

@ -5,6 +5,7 @@ import { KPIForm } from "../components/KPIForm";
import type { Kennzahl } from "../types/kpi";
import { API_HOST } from "../util/api";
export const Route = createFileRoute("/config-add")({
component: ConfigAddPage,
validateSearch: (search: Record<string, unknown>): { from?: string } => {
@ -47,19 +48,28 @@ function ConfigAddPage() {
body: JSON.stringify(kpiData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
navigate({ to: "/config" });
navigate({
to: "/config",
search: { success: "true", ...(from ? { from } : {}) },
});
} catch (error) {
console.error('Error creating KPI:', error);
throw error;
}
};
const handleCancel = () => {
navigate({ to: "/config" });
navigate({
to: "/config",
search: from ? { from } : undefined,
});
};
return (
@ -83,7 +93,7 @@ function ConfigAddPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Neue Kennzahl hinzufügen
@ -93,6 +103,7 @@ function ConfigAddPage() {
<KPIForm
mode="add"
key={Date.now()}
onSave={handleSave}
onCancel={handleCancel}
/>

View File

@ -1,5 +1,6 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { Box, Typography, IconButton, Button, CircularProgress, Paper, Divider
import {
Box, Typography, IconButton, Button, CircularProgress, Paper, Divider
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useEffect, useState } from "react";
@ -38,6 +39,7 @@ function KPIDetailPage() {
try {
setLoading(true);
const response = await fetch(`${API_HOST}/api/kpi_setting/${kpiId}`);
if (!response.ok) {
if (response.status === 404) {
setError('KPI not found');
@ -72,7 +74,6 @@ function KPIDetailPage() {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const updatedKennzahl = await response.json();
setKennzahl(updatedKennzahl);
setIsEditing(false);
@ -82,6 +83,7 @@ function KPIDetailPage() {
}
};
const handleCancel = () => {
setIsEditing(false);
};
@ -153,7 +155,7 @@ function KPIDetailPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Detailansicht
@ -192,18 +194,13 @@ function KPIDetailPage() {
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beschreibung
<Typography variant="h6" fontWeight="bold" mb={1}>
Erforderlich:
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.description || "Zurzeit ist die Beschreibung der Kennzahl leer. Klicken Sie auf den Bearbeiten-Button, um die Beschreibung zu ergänzen."}
<Typography variant="body1" sx={{ mb: 2, fontSize: 16 }}>
{kennzahl.mandatory ? 'Ja' : 'Nein'}
</Typography>
<Box mt={2}>
<Typography variant="body2" color="text.secondary">
<strong>Erforderlich:</strong> {kennzahl.mandatory ? 'Ja' : 'Nein'}
</Typography>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
@ -216,28 +213,6 @@ function KPIDetailPage() {
{typeDisplayMapping[kennzahl.type] || kennzahl.type}
</Typography>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Synonyme & Übersetzungen
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.translation || "Zurzeit gibt es keine Einträge für Synonyme und Übersetzungen der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."}
</Typography>
</Box>
<Divider sx={{ my: 3 }} />
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beispiele von Kennzahl
</Typography>
<Typography variant="body1" color="text.secondary">
{kennzahl.example || "Zurzeit gibt es keine Beispiele der Kennzahl. Klicken Sie auf den Bearbeiten-Button, um die Liste zu ergänzen."}
</Typography>
</Box>
</Paper>
</Box>
);
@ -264,7 +239,7 @@ function KPIDetailPage() {
>
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Kennzahl bearbeiten

View File

@ -3,18 +3,87 @@ import { Box, Button, IconButton, Typography } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useNavigate } from "@tanstack/react-router";
import { ConfigTable } from "../components/ConfigTable";
import { API_HOST } from "../util/api";
import Snackbar from "@mui/material/Snackbar";
import MuiAlert from "@mui/material/Alert";
import { useState, useEffect } from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Tooltip from "@mui/material/Tooltip";
export const Route = createFileRoute("/config")({
component: ConfigPage,
validateSearch: (search: Record<string, unknown>): { from?: string } => {
const from = typeof search.from === "string" ? search.from : undefined;
return { from };
validateSearch: (search: Record<string, unknown>): { from?: string; success?: string } => {
return {
from: typeof search.from === "string" ? search.from : undefined,
success: typeof search.success === "string" ? search.success : undefined
};
}
});
function ConfigPage() {
const navigate = useNavigate();
const { from } = Route.useSearch();
const { from, success } = Route.useSearch();
const [snackbarOpen, setSnackbarOpen] = useState(success === "true");
const [snackbarMessage, setSnackbarMessage] = useState<string>("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder zuerst weitere Kennzahlen hinzufügen.");
const [trainingRunning, setTrainingRunning] = useState(false);
const [hasUntrainedKPIs, setHasUntrainedKPIs] = useState(false);
const fetchKPISettings = async () => {
try {
const res = await fetch(`${API_HOST}/api/kpi/settings`);
const data = await res.json();
const untrainedExists = data.some((kpi: any) => {
return kpi.is_trained === false;
});
setHasUntrainedKPIs(untrainedExists);
} catch (err) {
console.error("Fehler beim Laden der KPIs:", err);
}
};
useEffect(() => {
fetchKPISettings();
}, []);
useEffect(() => {
if (success === "true") {
setTimeout(() => {
navigate({
to: "/config",
search: from ? { from } : undefined,
replace: true
});
}, 100);
}
}, [success]);
useEffect(() => {
const checkInitialTrainingStatus = async () => {
try {
const res = await fetch(`${API_HOST}/api/spacy/train-status`);
const data = await res.json();
if (data.running) {
setTrainingRunning(true);
pollTrainingStatus();
}
} catch (err) {
console.error("Initiale Trainingsstatus-Abfrage fehlgeschlagen", err);
}
};
checkInitialTrainingStatus();
}, []);
const handleAddNewKPI = () => {
navigate({
@ -31,7 +100,57 @@ function ConfigPage() {
}
};
const handleTriggerTraining = async () => {
setTrainingRunning(true);
try {
const response = await fetch(`${API_HOST}/api/spacy/train`, {
method: "POST",
});
if (!response.ok) throw new Error("Training konnte nicht gestartet werden");
// Erfolgsmeldung erst hier anzeigen
setSnackbarMessage("Training wurde gestartet.");
setSnackbarOpen(true);
pollTrainingStatus(); // jetzt starten
} catch (err) {
console.error(err);
setSnackbarMessage("Fehler beim Starten des Trainings.");
setSnackbarOpen(true);
setTrainingRunning(false);
}
};
const pollTrainingStatus = () => {
const interval = setInterval(async () => {
try {
const res = await fetch(`${API_HOST}/api/spacy/train-status`);
const data = await res.json();
console.log("Trainingsstatus:", data); //Debug-Ausgabe
if (!data.running) {
clearInterval(interval);
console.log("Training abgeschlossen Snackbar wird ausgelöst");
setSnackbarMessage("Training abgeschlossen!");
setSnackbarOpen(true);
setTrainingRunning(false);
fetchKPISettings();
}
} catch (err) {
console.error("Polling-Fehler:", err);
clearInterval(interval);
}
}, 3000);
};
return (
<>
<Box
minHeight="100vh"
width="100vw"
@ -49,14 +168,55 @@ function ConfigPage() {
alignItems="center"
px={4}
>
{/* Linke Seite: Zurück & Titel */}
<Box display="flex" alignItems="center">
<IconButton onClick={handleBack}>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }} />
</IconButton>
<Typography variant="h5" fontWeight="bold" ml={3}>
Konfiguration der Kennzahlen
</Typography>
</Box>
<Box display="flex" flexDirection="column" alignItems="flex-end" gap={1}>
<Box display="flex" gap={2}>
{trainingRunning || !hasUntrainedKPIs ? (
<Tooltip title="Alle Kennzahlen sind bereits trainiert.">
<span>
<Button
variant="contained"
onClick={handleTriggerTraining}
disabled
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
{trainingRunning ? (
<>
<CircularProgress size={20} sx={{ color: "white", mr: 1 }} />
Wird trainiert...
</>
) : (
"Neu trainieren"
)}
</Button>
</span>
</Tooltip>
) : (
<Button
variant="contained"
onClick={handleTriggerTraining}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
>
Neu trainieren
</Button>
)}
<Button
variant="contained"
onClick={handleAddNewKPI}
@ -68,9 +228,32 @@ function ConfigPage() {
Neue Kennzahl hinzufügen
</Button>
</Box>
</Box>
</Box>
{/* Tabelle */}
<Box sx={{ width: "100%", mt: 4, display: "flex", justifyContent: "center" }}>
<ConfigTable from={from} />
<ConfigTable from={from} trainingRunning={trainingRunning} />
</Box>
</Box>
{/* Snackbar */}
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
>
<MuiAlert
elevation={6}
variant="filled"
onClose={() => setSnackbarOpen(false)}
severity="success"
sx={{ width: "100%" }}
>
{snackbarMessage}
</MuiAlert>
</Snackbar>
</>
);
}

View File

@ -1,13 +1,17 @@
export interface Kennzahl {
id: number;
name: string;
description: string;
mandatory: boolean;
type: string;
translation: string;
example: string;
position: number;
active: boolean;
exampleText?: string;
markedValue?: string;
examples?: {
sentence: string;
value: string;
}[];
is_trained?: boolean;
}
export const typeDisplayMapping: Record<string, string> = {

View File

@ -5,6 +5,9 @@ import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact()],
build: {
chunkSizeWarningLimit: 1000, // default ist 500
},
test: {
globals: true,
environment: "jsdom",