Merge branch 'main' of gitMannheim:PSE2_FF/pse2_ff
commit
fd06fc1821
|
|
@ -0,0 +1,175 @@
|
|||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 5050
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# ExxetaGPT Microservice
|
||||
|
||||
## Lokaler Start (ohne Container)
|
||||
|
||||
### 1. Voraussetzungen
|
||||
|
||||
- Python 3.11+
|
||||
- Virtuelle Umgebung (empfohlen)
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. .env Datei erstellen
|
||||
Leg eine .env Datei im Projektverzeichnis mit der Exxeta API-Key an
|
||||
|
||||
(Der API Key ist ein JWT von Exxeta – nicht veröffentlichen!)
|
||||
|
||||
### 3. Starten
|
||||
python app.py
|
||||
|
||||
## Verwendung als Docker-Container
|
||||
|
||||
### 1. Build
|
||||
```bash
|
||||
docker build -t exxeta-gpt .
|
||||
```
|
||||
|
||||
### 2. Starten
|
||||
```bash
|
||||
docker run -p 5050:5050 --env-file .env exxeta-gpt
|
||||
```
|
||||
|
||||
## Beispielaufruf:
|
||||
```bash
|
||||
curl -X POST http://localhost:5050/extract \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @text-per-page.json
|
||||
```
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from flask import Flask, request, jsonify
|
||||
from services.extractExxeta import extract_with_exxeta
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/extract', methods=['POST'])
|
||||
def extract_text_from_ocr_json():
|
||||
json_ocr = request.get_json()
|
||||
json_data = extract_with_exxeta(json_ocr)
|
||||
return jsonify(json_data), 200
|
||||
#TO DO: Anbindung an das Merge & Validate Service
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5050, debug=True)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
python-dotenv
|
||||
flask
|
||||
requests
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import requests
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
MODEL = "gpt-4o-mini"
|
||||
EXXETA_BASE_URL= "https://ai.exxeta.com/api/v2/azure/openai"
|
||||
load_dotenv()
|
||||
EXXETA_API_KEY = os.getenv("API_KEY")
|
||||
|
||||
MAX_RETRIES = 3
|
||||
TIMEOUT = 30
|
||||
|
||||
def extract_with_exxeta(pages_json):
|
||||
results = []
|
||||
|
||||
for page_data in pages_json:
|
||||
page_num = page_data.get("page")
|
||||
text = page_data.get("text", "").strip()
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
if page_num == 1:
|
||||
prompt = (
|
||||
"Bitte extrahiere gezielt folgende drei Kennzahlen aus dem folgenden Pitchbook-Text:\n\n"
|
||||
"- FONDSNAME\n"
|
||||
"- FONDSMANAGER\n"
|
||||
"- DATUM\n\n"
|
||||
"Extrahiere **nur** diese drei Werte, wenn sie im Text explizit genannt werden.\n\n"
|
||||
"WICHTIG:\n"
|
||||
"- Gib exakt eine Entität pro Kennzahl an.\n"
|
||||
"- Falls eine Information nicht eindeutig erkennbar ist, lass sie weg.\n"
|
||||
"- Gib die Antwort als **JSON-Array** im folgenden Format zurück:\n\n"
|
||||
"[\n"
|
||||
" {\n"
|
||||
" \"label\": \"FONDSNAME\",\n"
|
||||
" \"entity\": \"...\",\n"
|
||||
f" \"page\": {page_num},\n"
|
||||
" },\n"
|
||||
" ...\n"
|
||||
"]\n\n"
|
||||
"Nur JSON-Antwort – keine Kommentare, keine Erklärungen.\n\n"
|
||||
f"TEXT:\n{text}"
|
||||
)
|
||||
else:
|
||||
prompt = (
|
||||
"Bitte extrahiere relevante Fondskennzahlen aus dem folgenden Pitchbook-Text. "
|
||||
"Analysiere den Text sorgfältig, um **nur exakt benannte und relevante Werte** zu extrahieren.\n\n"
|
||||
|
||||
"ZU EXTRAHIERENDE KENNZAHLEN (immer exakt wie unten angegeben):\n"
|
||||
"- FONDSNAME\n"
|
||||
"- FONDSMANAGER\n"
|
||||
"- AIFM (z. B. Name Kapitalverwaltungsgesellschaft)\n"
|
||||
"- DATUM\n"
|
||||
"- RISIKOPROFIL (z. B. CORE, CORE+, VALUE-ADDED, OPPORTUNISTISCH)\n"
|
||||
"- ARTIKEL (z. B. ARTIKEL 6, 8, 9)\n"
|
||||
"- ZIELRENDITE\n"
|
||||
"- RENDITE\n"
|
||||
"- ZIELAUSSCHÜTTUNG\n"
|
||||
"- AUSSCHÜTTUNG\n"
|
||||
"- LAUFZEIT\n"
|
||||
"- LTV\n"
|
||||
"- MANAGEMENTGEBÜHREN (ggf. mit Staffelung und Bezug auf NAV/GAV)\n"
|
||||
"- SEKTORENALLOKATION (z. B. BÜRO, LOGISTIK, WOHNEN... inkl. %-Angaben)\n"
|
||||
"- LÄNDERALLOKATION (z. B. DEUTSCHLAND, FRANKREICH, etc. inkl. %-Angaben)\n\n"
|
||||
|
||||
"WICHTIG:\n"
|
||||
"- Gib **nur eine Entität pro Kennzahl** an – keine Listen oder Interpretationen.\n"
|
||||
"- Wenn mehrere Varianten genannt werden (z. B. \"Core und Core+\"), gib sie im Originalformat als **eine entity** an.\n"
|
||||
"- **Keine Vermutungen oder Ergänzungen**. Wenn keine Information enthalten ist, gib die Kennzahl **nicht aus**.\n"
|
||||
"- Extrahiere **nur wörtlich vorkommende Inhalte** (keine Berechnungen, keine Zusammenfassungen).\n"
|
||||
"- Jeder gefundene Wert muss einem der obigen Label **eindeutig zuordenbar** sein.\n\n"
|
||||
|
||||
"FORMAT:\n"
|
||||
"Antworte als **reines JSON-Array** mit folgendem Format:\n"
|
||||
"[\n"
|
||||
" {\n"
|
||||
" \"label\": \"Kennzahlname (exakt wie oben)\",\n"
|
||||
" \"entity\": \"Wert aus dem Text (exakt im Original)\",\n"
|
||||
f" \"page\": {page_num},\n"
|
||||
" },\n"
|
||||
" ...\n"
|
||||
"]\n\n"
|
||||
|
||||
f"Falls keine Kennzahl enthalten ist, gib ein leeres Array [] zurück.\n\n"
|
||||
f"Nur JSON-Antwort – keine Kommentare, keine Erklärungen, kein Text außerhalb des JSON.\n\n"
|
||||
f"TEXT:\n{text}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {EXXETA_API_KEY}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": "Du bist ein Finanzanalyst, der Fondsprofile auswertet. Antworte nur mit validen JSON-Arrays."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"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.raise_for_status()
|
||||
|
||||
content = response.json()["choices"][0]["message"]["content"]
|
||||
content = content.strip()
|
||||
|
||||
if content.startswith("```json"):
|
||||
content = content.split("```json")[1]
|
||||
if content.endswith("```"):
|
||||
content = content.split("```")[0]
|
||||
content = content.strip()
|
||||
|
||||
try:
|
||||
page_results = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
page_results = []
|
||||
|
||||
if isinstance(page_results, list):
|
||||
results.extend(page_results)
|
||||
break
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
if attempt == MAX_RETRIES:
|
||||
results.extend([])
|
||||
except Exception as e:
|
||||
if attempt == MAX_RETRIES:
|
||||
results.extend([])
|
||||
|
||||
json_result = json.dumps(results, indent=2, ensure_ascii=False)
|
||||
return json_result
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Node modules (React/JavaScript)
|
||||
node_modules/
|
||||
|
||||
# Build output (React)
|
||||
dist/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Mac/Windows system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
g++ \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY .. /app
|
||||
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN python -m spacy download en_core_web_sm
|
||||
|
||||
CMD ["python3.12", "app.py"]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from flask import Flask, request, jsonify
|
||||
from services.extract import extract
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/extraction', methods=['POST'])
|
||||
def extract_pdf():
|
||||
json_ocr = request.get_json()
|
||||
json_data = extract(json_ocr)
|
||||
return jsonify(json_data), 200
|
||||
#TO DO: Anbindung an das Merge & Validate Service
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5050)
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
spacy>=3.8.0,<3.9.0
|
||||
spacy-transformers==1.3.3
|
||||
transformers==4.35.2
|
||||
torch
|
||||
flask
|
||||
https://github.com/explosion/spacy-models/releases/download/xx_ent_wiki_sm-3.8.0/xx_ent_wiki_sm-3.8.0-py3-none-any.whl
|
||||
Binary file not shown.
|
|
@ -0,0 +1,30 @@
|
|||
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-best")
|
||||
nlp = spacy.load(model_path)
|
||||
|
||||
|
||||
def extract(pages_json):
|
||||
|
||||
results = []
|
||||
|
||||
for page in pages_json:
|
||||
text = page.get("text", "").strip()
|
||||
page_num = page.get("page")
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
spacy_result = nlp(text)
|
||||
for ent in spacy_result.ents:
|
||||
results.append({
|
||||
"label": ent.label_,
|
||||
"entity": ent.text,
|
||||
"page": page_num
|
||||
})
|
||||
|
||||
json_result = json.dumps(results, indent=2, ensure_ascii=False)
|
||||
return json_result
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# 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}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
[paths]
|
||||
train = "./data/train.spacy"
|
||||
dev = "./data/train.spacy"
|
||||
vectors = null
|
||||
init_tok2vec = null
|
||||
|
||||
[system]
|
||||
gpu_allocator = null
|
||||
seed = 0
|
||||
|
||||
[nlp]
|
||||
lang = "de"
|
||||
pipeline = ["tok2vec","ner"]
|
||||
batch_size = 1000
|
||||
disabled = []
|
||||
before_creation = null
|
||||
after_creation = null
|
||||
after_pipeline_creation = null
|
||||
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||
|
||||
[components]
|
||||
|
||||
[components.ner]
|
||||
factory = "ner"
|
||||
incorrect_spans_key = null
|
||||
moves = null
|
||||
scorer = {"@scorers":"spacy.ner_scorer.v1"}
|
||||
update_with_oracle_cut_size = 100
|
||||
|
||||
[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}
|
||||
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"
|
||||
width = 96
|
||||
depth = 4
|
||||
window_size = 1
|
||||
maxout_pieces = 3
|
||||
|
||||
[corpora]
|
||||
|
||||
[corpora.dev]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.dev}
|
||||
max_length = 0
|
||||
gold_preproc = false
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[corpora.train]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.train}
|
||||
max_length = 0
|
||||
gold_preproc = false
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[training]
|
||||
dev_corpus = "corpora.dev"
|
||||
train_corpus = "corpora.train"
|
||||
seed = ${system.seed}
|
||||
gpu_allocator = ${system.gpu_allocator}
|
||||
dropout = 0.1
|
||||
accumulate_gradient = 1
|
||||
patience = 1600
|
||||
max_epochs = 0
|
||||
max_steps = 20000
|
||||
eval_frequency = 200
|
||||
frozen_components = []
|
||||
annotating_components = []
|
||||
before_to_disk = null
|
||||
before_update = null
|
||||
|
||||
[training.batcher]
|
||||
@batchers = "spacy.batch_by_words.v1"
|
||||
discard_oversize = false
|
||||
tolerance = 0.2
|
||||
get_length = null
|
||||
|
||||
[training.batcher.size]
|
||||
@schedules = "compounding.v1"
|
||||
start = 100
|
||||
stop = 1000
|
||||
compound = 1.001
|
||||
t = 0.0
|
||||
|
||||
[training.logger]
|
||||
@loggers = "spacy.ConsoleLogger.v1"
|
||||
progress_bar = false
|
||||
|
||||
[training.optimizer]
|
||||
@optimizers = "Adam.v1"
|
||||
beta1 = 0.9
|
||||
beta2 = 0.999
|
||||
L2_is_weight_decay = true
|
||||
L2 = 0.01
|
||||
grad_clip = 1.0
|
||||
use_averages = false
|
||||
eps = 0.00000001
|
||||
learn_rate = 0.001
|
||||
|
||||
[training.score_weights]
|
||||
ents_f = 1.0
|
||||
ents_p = 0.0
|
||||
ents_r = 0.0
|
||||
ents_per_type = null
|
||||
|
||||
[pretraining]
|
||||
|
||||
[initialize]
|
||||
vectors = ${paths.vectors}
|
||||
init_tok2vec = ${paths.init_tok2vec}
|
||||
vocab_data = null
|
||||
lookups = null
|
||||
before_init = null
|
||||
after_init = null
|
||||
|
||||
[initialize.components]
|
||||
|
||||
[initialize.tokenizer]
|
||||
Binary file not shown.
|
|
@ -0,0 +1,145 @@
|
|||
[paths]
|
||||
train = "./data/train.spacy"
|
||||
dev = "./data/train.spacy"
|
||||
vectors = null
|
||||
init_tok2vec = null
|
||||
|
||||
[system]
|
||||
gpu_allocator = null
|
||||
seed = 0
|
||||
|
||||
[nlp]
|
||||
lang = "de"
|
||||
pipeline = ["tok2vec","ner"]
|
||||
batch_size = 1000
|
||||
disabled = []
|
||||
before_creation = null
|
||||
after_creation = null
|
||||
after_pipeline_creation = null
|
||||
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||
|
||||
[components]
|
||||
|
||||
[components.ner]
|
||||
factory = "ner"
|
||||
incorrect_spans_key = null
|
||||
moves = null
|
||||
scorer = {"@scorers":"spacy.ner_scorer.v1"}
|
||||
update_with_oracle_cut_size = 100
|
||||
|
||||
[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}
|
||||
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"
|
||||
width = 96
|
||||
depth = 4
|
||||
window_size = 1
|
||||
maxout_pieces = 3
|
||||
|
||||
[corpora]
|
||||
|
||||
[corpora.dev]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.dev}
|
||||
max_length = 0
|
||||
gold_preproc = false
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[corpora.train]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.train}
|
||||
max_length = 0
|
||||
gold_preproc = false
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[training]
|
||||
dev_corpus = "corpora.dev"
|
||||
train_corpus = "corpora.train"
|
||||
seed = ${system.seed}
|
||||
gpu_allocator = ${system.gpu_allocator}
|
||||
dropout = 0.1
|
||||
accumulate_gradient = 1
|
||||
patience = 1600
|
||||
max_epochs = 0
|
||||
max_steps = 20000
|
||||
eval_frequency = 200
|
||||
frozen_components = []
|
||||
annotating_components = []
|
||||
before_to_disk = null
|
||||
before_update = null
|
||||
|
||||
[training.batcher]
|
||||
@batchers = "spacy.batch_by_words.v1"
|
||||
discard_oversize = false
|
||||
tolerance = 0.2
|
||||
get_length = null
|
||||
|
||||
[training.batcher.size]
|
||||
@schedules = "compounding.v1"
|
||||
start = 100
|
||||
stop = 1000
|
||||
compound = 1.001
|
||||
t = 0.0
|
||||
|
||||
[training.logger]
|
||||
@loggers = "spacy.ConsoleLogger.v1"
|
||||
progress_bar = false
|
||||
|
||||
[training.optimizer]
|
||||
@optimizers = "Adam.v1"
|
||||
beta1 = 0.9
|
||||
beta2 = 0.999
|
||||
L2_is_weight_decay = true
|
||||
L2 = 0.01
|
||||
grad_clip = 1.0
|
||||
use_averages = false
|
||||
eps = 0.00000001
|
||||
learn_rate = 0.001
|
||||
|
||||
[training.score_weights]
|
||||
ents_f = 1.0
|
||||
ents_p = 0.0
|
||||
ents_r = 0.0
|
||||
ents_per_type = null
|
||||
|
||||
[pretraining]
|
||||
|
||||
[initialize]
|
||||
vectors = ${paths.vectors}
|
||||
init_tok2vec = ${paths.init_tok2vec}
|
||||
vocab_data = null
|
||||
lookups = null
|
||||
before_init = null
|
||||
after_init = null
|
||||
|
||||
[initialize.components]
|
||||
|
||||
[initialize.tokenizer]
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"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":{
|
||||
"tok2vec":[
|
||||
|
||||
],
|
||||
"ner":[
|
||||
"AUSSCH\u00dcTTUNGSRENDITE",
|
||||
"Aussch\u00fcttungsrendite",
|
||||
"Laufzeit",
|
||||
"RISIKOPROFIL",
|
||||
"Risikoprofil"
|
||||
]
|
||||
},
|
||||
"pipeline":[
|
||||
"tok2vec",
|
||||
"ner"
|
||||
],
|
||||
"components":[
|
||||
"tok2vec",
|
||||
"ner"
|
||||
],
|
||||
"disabled":[
|
||||
|
||||
],
|
||||
"performance":{
|
||||
"ents_f":0.8888888889,
|
||||
"ents_p":0.8205128205,
|
||||
"ents_r":0.9696969697,
|
||||
"ents_per_type":{
|
||||
"RISIKOPROFIL":{
|
||||
"p":1.0,
|
||||
"r":0.9705882353,
|
||||
"f":0.9850746269
|
||||
},
|
||||
"Risikoprofil":{
|
||||
"p":0.8,
|
||||
"r":1.0,
|
||||
"f":0.8888888889
|
||||
},
|
||||
"Laufzeit":{
|
||||
"p":0.9,
|
||||
"r":1.0,
|
||||
"f":0.9473684211
|
||||
},
|
||||
"AUSSCH\u00dcTTUNGSRENDITE":{
|
||||
"p":0.5925925926,
|
||||
"r":0.9411764706,
|
||||
"f":0.7272727273
|
||||
},
|
||||
"Aussch\u00fcttungsrendite":{
|
||||
"p":0.6666666667,
|
||||
"r":1.0,
|
||||
"f":0.8
|
||||
}
|
||||
},
|
||||
"tok2vec_loss":119.7162696429,
|
||||
"ner_loss":824.8371582031
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
‚¥movesÚì{"0":{},"1":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8},"2":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8},"3":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8},"4":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8,"":1},"5":{"":1}}£cfg<66>§neg_keyÀ
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"mode":"default"
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
[paths]
|
||||
train = "./data/train.spacy"
|
||||
dev = "./data/train.spacy"
|
||||
vectors = null
|
||||
init_tok2vec = null
|
||||
|
||||
[system]
|
||||
gpu_allocator = null
|
||||
seed = 0
|
||||
|
||||
[nlp]
|
||||
lang = "de"
|
||||
pipeline = ["tok2vec","ner"]
|
||||
batch_size = 1000
|
||||
disabled = []
|
||||
before_creation = null
|
||||
after_creation = null
|
||||
after_pipeline_creation = null
|
||||
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
|
||||
vectors = {"@vectors":"spacy.Vectors.v1"}
|
||||
|
||||
[components]
|
||||
|
||||
[components.ner]
|
||||
factory = "ner"
|
||||
incorrect_spans_key = null
|
||||
moves = null
|
||||
scorer = {"@scorers":"spacy.ner_scorer.v1"}
|
||||
update_with_oracle_cut_size = 100
|
||||
|
||||
[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}
|
||||
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"
|
||||
width = 96
|
||||
depth = 4
|
||||
window_size = 1
|
||||
maxout_pieces = 3
|
||||
|
||||
[corpora]
|
||||
|
||||
[corpora.dev]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.dev}
|
||||
max_length = 0
|
||||
gold_preproc = false
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[corpora.train]
|
||||
@readers = "spacy.Corpus.v1"
|
||||
path = ${paths.train}
|
||||
max_length = 0
|
||||
gold_preproc = false
|
||||
limit = 0
|
||||
augmenter = null
|
||||
|
||||
[training]
|
||||
dev_corpus = "corpora.dev"
|
||||
train_corpus = "corpora.train"
|
||||
seed = ${system.seed}
|
||||
gpu_allocator = ${system.gpu_allocator}
|
||||
dropout = 0.1
|
||||
accumulate_gradient = 1
|
||||
patience = 1600
|
||||
max_epochs = 0
|
||||
max_steps = 20000
|
||||
eval_frequency = 200
|
||||
frozen_components = []
|
||||
annotating_components = []
|
||||
before_to_disk = null
|
||||
before_update = null
|
||||
|
||||
[training.batcher]
|
||||
@batchers = "spacy.batch_by_words.v1"
|
||||
discard_oversize = false
|
||||
tolerance = 0.2
|
||||
get_length = null
|
||||
|
||||
[training.batcher.size]
|
||||
@schedules = "compounding.v1"
|
||||
start = 100
|
||||
stop = 1000
|
||||
compound = 1.001
|
||||
t = 0.0
|
||||
|
||||
[training.logger]
|
||||
@loggers = "spacy.ConsoleLogger.v1"
|
||||
progress_bar = false
|
||||
|
||||
[training.optimizer]
|
||||
@optimizers = "Adam.v1"
|
||||
beta1 = 0.9
|
||||
beta2 = 0.999
|
||||
L2_is_weight_decay = true
|
||||
L2 = 0.01
|
||||
grad_clip = 1.0
|
||||
use_averages = false
|
||||
eps = 0.00000001
|
||||
learn_rate = 0.001
|
||||
|
||||
[training.score_weights]
|
||||
ents_f = 1.0
|
||||
ents_p = 0.0
|
||||
ents_r = 0.0
|
||||
ents_per_type = null
|
||||
|
||||
[pretraining]
|
||||
|
||||
[initialize]
|
||||
vectors = ${paths.vectors}
|
||||
init_tok2vec = ${paths.init_tok2vec}
|
||||
vocab_data = null
|
||||
lookups = null
|
||||
before_init = null
|
||||
after_init = null
|
||||
|
||||
[initialize.components]
|
||||
|
||||
[initialize.tokenizer]
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"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":{
|
||||
"tok2vec":[
|
||||
|
||||
],
|
||||
"ner":[
|
||||
"AUSSCH\u00dcTTUNGSRENDITE",
|
||||
"Aussch\u00fcttungsrendite",
|
||||
"Laufzeit",
|
||||
"RISIKOPROFIL",
|
||||
"Risikoprofil"
|
||||
]
|
||||
},
|
||||
"pipeline":[
|
||||
"tok2vec",
|
||||
"ner"
|
||||
],
|
||||
"components":[
|
||||
"tok2vec",
|
||||
"ner"
|
||||
],
|
||||
"disabled":[
|
||||
|
||||
],
|
||||
"performance":{
|
||||
"ents_f":0.8780487805,
|
||||
"ents_p":0.9473684211,
|
||||
"ents_r":0.8181818182,
|
||||
"ents_per_type":{
|
||||
"RISIKOPROFIL":{
|
||||
"p":1.0,
|
||||
"r":0.9705882353,
|
||||
"f":0.9850746269
|
||||
},
|
||||
"Risikoprofil":{
|
||||
"p":0.8,
|
||||
"r":1.0,
|
||||
"f":0.8888888889
|
||||
},
|
||||
"AUSSCH\u00dcTTUNGSRENDITE":{
|
||||
"p":0.7777777778,
|
||||
"r":0.4117647059,
|
||||
"f":0.5384615385
|
||||
},
|
||||
"Laufzeit":{
|
||||
"p":1.0,
|
||||
"r":1.0,
|
||||
"f":1.0
|
||||
},
|
||||
"Aussch\u00fcttungsrendite":{
|
||||
"p":1.0,
|
||||
"r":0.5,
|
||||
"f":0.6666666667
|
||||
}
|
||||
},
|
||||
"tok2vec_loss":235.8388520621,
|
||||
"ner_loss":1878.9451904297
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
‚¥movesÚì{"0":{},"1":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8},"2":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8},"3":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8},"4":{"RISIKOPROFIL":91,"AUSSCH\u00dcTTUNGSRENDITE":40,"Laufzeit":26,"Risikoprofil":10,"Aussch\u00fcttungsrendite":8,"":1},"5":{"":1}}£cfg<66>§neg_keyÀ
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"mode":"default"
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
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-LTY‚Aktueller 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"]]},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import os
|
||||
import spacy
|
||||
|
||||
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")
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
[
|
||||
{
|
||||
"page": 1,
|
||||
"text": "Real Estate Prime Europe\nAccess the Core of European Prime Cities with a green SRI fund\nincluding a genuine low carbon commitment\nFor professional investors only. Not for further distribution. Information is valid as of December 2018"
|
||||
},
|
||||
{
|
||||
"page": 2,
|
||||
"text": "Content\n1. Executive Summary\n. GRD Real Estate\nThe Fund\nExpertise & Investment Process\nInvestment Case & Views\n»wp\nao\nAppendices\nFund Investment Process\nos Detailed SRI Process\n— Reporting\n— Biographies\n2 See : ::: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 3,
|
||||
"text": "01\nExecutive summary\n3 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 4,
|
||||
"text": "Executive summary\nA selective pan European investment strategy\n- European economics in most countries should sustain Office leasing\nactivity, retail consumption and business Hotels. Considering the\ndepth of these market segments, selectivity will be key.\n- A strategy targeting high quality Core and Core+ buildings, with\ndefined SRI objectives, in order to extract value through an active\nasset management.\nWhat makes us different?\n- Se, ss: long established real estate investment\nand fund management player in Europe, allowing for a clear view of\nopportunities coming to the market (€4bn of acquisitions out of\nFrance over the past 3 years).\n- GRD semi-open architecture based on a strong integrated\nplatform in France, Luxembourg and Italy and long standing\npartnerships in major European countries gives us the required\nlocal historical expertise and the important flexibility to build and\ndiligently manage a well balanced pan European portfolio.\n- GRDas a leading player in ESG expertise is putting all\n| resources to structure products in line with our ambition: AREPE will\na be a low carbon fund compliant with the Qi Real Estate\nsustainable investment Charter.\n4 GE: :: Estate Prime Europe EEE\n|—________|”|\"_)"
|
||||
},
|
||||
{
|
||||
"page": 5,
|
||||
"text": "02\nSERes|i Estate"
|
||||
},
|
||||
{
|
||||
"page": 6,
|
||||
"text": "A leader in European real estate\nA LEADER IN EUROPEAN PRIME CITIES €33 bn 125 750\nOf AuM Dedicated people Properties in Europe\nGREED Real Estate is a company specialized in developing, i.\nstructuring and managing European focused property funds. Gr: — = een\nThanks to the power of its inflows carried out the largest\ntransactions in the European market, with €4bn of acquisitions out of\nFrance over the past 3 years.\n- WEB sources assets across Europe, structures the acquisitions\nand their financing, and manages all type of properties with a focus on\nOffices. 700+ properties in France, Italy, Germany, the Netherlands,\nUK, Czech Republic, Luxembourg...\nACOMPLETE OFFERING\nSee the map with detail for each of the 750 assets on:\n- Commingled Funds (closed-end and open-ended); Dedicated Funds; Club Deals\n& Joint Ventures ; Mandates (tailor made solution).\n- A leading player in managing and structuring regulated funds in France. mOffice 75,6%\nBRetail 13,8%\n- A window for international clients looking to access the European real estate\nEB Hotels 4,3%\nmarket for diversification\n\" Industrial/logistics 1,0%\n\" Residentials 9.4%\nOthers 4,9%\nove > 201:\n6 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 7,
|
||||
"text": "Comprehensive service offering to institutions\nDirect Investment Indirect Investment\nDedicated\nClub Deals Mandates Funds Commingled Funds\nClub Deals: investing with similar-minded investors Different strategies (single country or pan-European, single\nasset class or diversified)\nMandates: build from a tailor made proposal, defined according ; ;\nto the client’s constraints & guidelines Different risk profile (core, core+, value-added)\nDedicated funds: build a structure to manage client’s assets Different leverage levels\nDifferent jurisdictions (French OPCI, Lux. SIFs, etc.)\nSESE\nT GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 8,
|
||||
"text": "SEBeal Estate : a genuine low carbon commitment\nA fully documented sustainable investment charter\nEnvironmental and social performance\nEach of our assets is ranked by our dedicated team, from A to G.\nG F E D C B A\nWe use a well-known referential to perform a thorough analysis of 0 14 29 43 57 86 100\neach asset: the BREEAM In Use Part 1 international referential is fully Pollution\n59% Health & wellbeing\nexploited in order to perform an analysis of the environmental and social 52%\nperformance of the assets. Land we\n100%\nWaste\n100%\nEnergy\nGERD::| Estate has also developed additional and specific tools Materials 65%\n51%\nto complete and strengthen our sustainable approach on an asset by Water\n78% Transport\nasset basis: 23%\nThe Carbon Footprint 2°C Trajectory Climate Risk\nThis footprint is calculated based on: This trajectory will help assess the This evaluation shows the exposure of\nEnergy and leaks of refrigerants (scope 1) greenhouse gas emissions’ reductions the asset to different climate related risks\nElectricity, water and energy consumption needed to respect the Paris agreement (sea level, floods, temperature,\n(scope 2) heatwaves, storms...)\nMaterials used for construction or\nrefurbishment (Scope 3)\nGED Real Estate has built a genuine low carbon approach, in line with the best\nstandards\n8 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 9,
|
||||
"text": "The Fund\nSE Real Estate Prime Europe\n9 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 10,
|
||||
"text": "The Fund\nFund Objective\nNavigate the diversity of the Core/Core+ investment opportunities\nin European Prime Cities\nGEDis an open-ended Lux-based fund providing an attractive core/core+ real estate exposure, leveraging\nGRRE expertise in European RE markets. It offers diversification in terms of pan-European geographies and\nsectors: Offices, Retail and Hotels.\n- Risk level: Core/Core+\n- Type: essentially new or recently refurbished assets\nStrategy\n- Individual asset size: ranging from € SOM to € 200M\ncharacteristics\n- Leverage: 50% max (at asset and fund level) / Target 40-45%\n- A low carbon approach fully compliant with the best standards\n- Sector: offices (70% target; 60% min), all other types (30% target; 40% max)\n- Geography: 100% Europe\n- Tier 1: Min 75% in total, Max 40% by country (FR, UK, DE, BE, NL, LU, Nordics,\nAllocation SP, IT, CH)\nGuidelines - Tier 2: Max 25% in total in rest of Europe, max 10% by country\n- Non Euro investments: max 20%\n- Manage to Core: max 20%\n- Concentration limits: max 25% by asset\nTarget Returns - IRR: 6% - 7%\n(net of fees and tax) - Cash on Cash: 4% - 5%\nSt\n10 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 11,
|
||||
"text": "The caine\n4aodPr.\nFund Key Features\nA Luxembourg Alternative Investment Fund\ndenominated in EUR\nTarget Size: Target equity of€ 500m (€1 Bn of assets)\nRegulatory Qualification: Luxembourg AIF\nFund Structure “ Legal Form: Luxembourg Limited Partnership\nCurrency: EUR\nMinimum Investment: €5m\nSubscriptions: Quarterly. Queue system, with pari passu calls to the oldest vintage.\nRedemptions: Every semester (Dec and June). Queue system by vintage after lock up period.\nLock up: 5 years for investors entering in 2019, 4 years for investors entering in 2020 then 3\nyears\nLiquidity - Gates: 10% of NAV per annum\nRedemption deadline: 18 months after demand\nRedemption fee: If there are sufficient inflows when redemptions are outstanding, no fee on\nredemption ; otherwise, redemption fee will be 3% (10Y holding period), 2% (15Y) or 1% (>15Y)\nof NAV\nAsset Management/ Fund Management: sliding scale considering committed amount\n55-50-40bp x NAV (5-10; 10-50; 50+ ME). Fee only payable on investment called.\nFees For investors committing before 31/12/2019: Rebate of 10bp for 5 years after subscription\n(before VAT) = Acquisition: 0.5% to 1% x GAV following asset size\nDisposal: 200k€, flat\nPerformance fee: 20% above 7% IRR (payable by investors on realised profit)\n11 GE : :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 12,
|
||||
"text": "The Fund\nWhy a Core / Core + investment program?\nBenefits of the core\n/ core+ segment\n- Capital preservation*\n- Room to lock in Risk premia over\nfinancing rates\nDrawbacks ofthe core\n- Assets attractive for Long Term / core+ segment\nfinancing\n- Strong competition on this segment of\n- Assets adapted to a Buy and Hold the market for investment\nstrategy\n- Real Estate yields testing historically\n- Assets that generate running yields low levels\n- Adeep market improving availability\n- Core assets leave less room for active\nand asset liquidity\nasset management value creation\n- Better resilience to potential interest\nrate hikes (which usually triggers flight\nto quality)\n*capital preservation is defined here as a characteristic of core/core+ investments. There is no guarantee of capital.\nSESE\n12 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 13,
|
||||
"text": "The Fund\nSees has a strong track record versus the MSCI PEPFI*\n% 2015 2016 2017 2018 3Y 3Y (unl.) M S C Pas\nGHEE Total return 5,3 16,1 13,6 8,9 12,8 8,0 =\nLTV 71,0 62,4 58,9 58,2 59,8 0,0\nPercentile 80th 5th 5th 10th 5th\nPEPFI Total return 8,2 5,7 6,4 6,4 6,2 5,3\nLTV 20,6 21,5 21,9 21,8 21,7 0,0\nComments\n° LTV: part of the performance is down to leverage and ap ; significantly more leveraged than the benchmark and will remain so even\nafter we have reduced leverage to ca. 45%. Given the low cost of debt today, we believe this level of leverage makes sense: it boost\nincome return while the core nature of the portfolio should dampen a capital loss in case of a market downturn.\n. Asset-mix: QD current allocation is 100% Germany and a mixture of retail/office/hotel. As such, its asset and country allocation is less\nrisky than the benchmark which includes more risky countries and asset types (eg industrial/logistics).\n*PEPFI: Pan European Property Fund index QT: state as at end of December 2018\n16 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 14,
|
||||
"text": "Indicative pipeline of Investments — June 2019\nTechnical\nCountry / city Sector Risk Deal size Location Tenancy Comments\nspecificities\nOffice building 500m away from the European\nBELGIUM Offices Core Excellent New building Vacant Commission - well served by the subway (250m away\n80m\nBrussels Delivery Q4 2020 from one of the major metro station - 67 parking spaces -\nDelivery Q4 2020\nMulti let office building in the Sought after brussel's\nBELGIUM Fully let\nOffices Core 40m Good Recent building Euopean District - 100% let to 8 tenants - WALB 5,42\nBrussels WALB 5,42\nyears - Share deal - NIY around 4,17%\nFully let Office building in the South of Madrid - - 7 years lease\nSPAIN Completely\nOffices Core <50m Good Single tenant recently signed by an energy company - 47 parking\nMadrid refurbished\nspaces - Built in 1891 refurbished in 2019 -\nFully let\nFRANCE New building\nOffices Core 400m Good Very well located — LT lease - very good tenants\nLevallois Single tenant\nFRANCE New building Fully let\nOffices Core 300m Good New building in Paris\nParis 14 Multi tenant\n2 independent and interconnected office buildings in\nBELGIUM\nOffices Core 99m-102m Excellent Refurbished in 2014 Occupancy 92% Leopold / Location : A/ Accessibility : very good, in front\nBrussels\nof the property / Construction : 1992 / WALT : 7 years/\nLocated in the heart of CBD - next to the metro stop\nNETHERLANDS Beurs - Single good tenant - comprehensive\nOffices Core 85m-90m Excellent Recent building Fully let\nRotterdam refurbishment undergoing - handover scheduled for June\n2020 - 207 parking spaces\nLocated in the port of Rotterdam - connected with the\nFully let\nNETHERLANDS A15 highway - Sale and lease back with 10 years triple-\nLogistics Core 50m - 55m Good New building\nRotterdam net lease agreement - 43 loading docks - Expected\nSingle tenant\ncompletion in Q1 2020\n17 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 15,
|
||||
"text": "Indicative pipeline of Investments — June 2019\n; Technical\nCountry / city Sector Risk Deal size Location De Tenancy Comments\nspecificities\nThe building is a recently constructed office property in\nthe European District of the Brussels CBD. Located close\nFully letto five\ntothe Rue de la Loi, the main axis of the district, the\nBELGIUM Offices Core Good New building tenants (WALT of\n34-40m building benefits from close proximtio ttyhe main EU\nBrussels almost 10 Y)\ninstitutions, excellent infrastructure and many amenities.\nSpecifications fully meet today's market requirements,\nincluding a BREEAM Excellent rating.\nCompletely Fully let Office located in the West Berlin — Good Tenant -\nGERMANY Offices Core 44m-46m Se refurbished\nSingle tenant Delivery Q4 2020 - Lease term 20 years -\nBerlin\nA recent office building constructed in 2017, with 7 700\nsqm, 59 parking spaces and amenities (conference\nAlmost fully let\ncentre, fitness club, cafe, restaurant...). The property has\nPOLAND (WAULT 10 years)\nOffices Core 50-75m Excellent Recent building a Leed Platinium certification. The building is very well\nWarsaw\nlocated in the City Center of Warsaw and benefits from\nMulti tenant\nan excellent access to public transport. The expected\nyield is around 4,50%.\nNew 17 700 sqm development completed at the end of\nFully rented 2019, located seaside, in the South West city center of\nFINLAND\nOffices Core Good New building Helsinki. The property is single let to an Agency of the\nHelsinki 100-150m\nSingle tenant European Union on a 10 year lease agreement. The\nexpected yield is around 4,50%.\nAn outstanding quality iconic and sensitively developed\nFully rented\nCZECH Republic multifunctional building consisted of a restored baroque-\nOffices Core CBD New building\nPrague 90-100m renaissance palace from 1734, juxtaposed with a 2018\nMulti tenants\nconstructed eight storey premium office building\nFully rented\nFRANCE Property newly built in an established office submarket\nOffices Core Good New building\nSaint Denis 150 -170m close to public transports.\nMulti tenants\n18 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 16,
|
||||
"text": "Expertise\nOur strategy in Europe\nInvestment Process\nESG framework\nTeams\n19 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 17,
|
||||
"text": "Strategy to access & select prime assets in Europe\nGED benefits from long history, strong local partnerships, global and CRE economic research\nAcquisitions 2016-2018: over €11bn\nDeep deal flow in Europe = France\n= Paris exceptional deals\n\\\n= Germany\nGERM sources assets across Europe. All segments of\n= Netherlands\nreal estate assets are covered, with a focus on offices. = Italy\nThanks to the importance of its inflows, @jiiicarries = Austria\nout the largest transactions in the European market, Rane Rep\nwith €4bn of acquisitions out of France over the past 3 LUXENDBUG\nyears. en\n= UK\n= Finland\n2015 2016 2017 2018\nPipeline 620 assets analysed 520 assets analysed 577 assets analysed 813 assets analysed\nP €41.6 Bn €51.6 Bn €63.8 Bn €66.3Bn\nee 126 assets 74 assets 86 assets 79 assets\nCommittee €5.7 Bn €11.1 Bn €17.3 Bn €8.8 Bn\nAegucit 52 assets 55 assets 30 assets 17 assets\ncq7u isitions €2.6 Bn €4.3 Bn €6.0 Bn* €1.1 Bn\n20 GE: :: Estate Prime Europe EEE"
|
||||
},
|
||||
{
|
||||
"page": 18,
|
||||
"text": "An open-architecture organisation\nAllowing for flexibility and agility\n- Semi-open architecture based on a strong\nintegrated platform in France, Luxembourg,\nItaly;\n- Longstanding partnerships in major\nEuropean countries, giving us the required local\nexpertise and the important flexibility to choose\nwhere to invest in Europe;\n- A strict and documented methodology when\nselecting our partners, in terms of compliance\nwith our ESG policy (target 2021).\nCountry Partners (non-exclusive)\nGermany Etoile Properties\nAerium\nIC Property Investment &\nManagement\nBenelux Etoile Properties\nHannover Leasing\nUK Knight Frank\nScandinavia Newsec\nAustria EHL\nIberia Etoile Properties\nFY Estate, December 2018\nEEE\n21 GD : :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 19,
|
||||
"text": "Investment process\nSR: Estate Prime Europe\nSourcing - Continuous market watch: discussions with our local partners, brokers, sellers\n- Dealflow in open architecture\n- First screening, analysis, identification of due diligence issues\n- Target portfolio guidelines\n- Thorough financial and ESG analysis of the asset, external valuation and\nasset visit\n- Technical, legal, tax, notarial due diligence\n- Negotiation of financing in accordance with due diligences’\nconclusions (tender, term sheet, loan documentation)\n- Acquisition structuring to minimize risk and tax, closing\ndocumentation\n- Strategy & business plan\n- Coordination with property and asset\nmanager\n22 GEDea! Estate Prime Europe ZEEEEE"
|
||||
},
|
||||
{
|
||||
"page": 20,
|
||||
"text": "GEBhas a long experience of core/core+ investments\nin Europe\nGERD(a balanced pan-European open ended retail fund — under the form of a French collective undertaking for Real\nEstate investments “OPCI”) is the flagship oQf in France and combines RE and listed assets (respective targets of 60%\nand 40%) with max. 40% leverage. The RE portfolio of the fund is a good illustration Of expertise in European\ncore/core+ investments.\nOPCIMMO RE EN ee,\nportfolio ) ZZ\nTotal Return +4,6% +6,6% +7,5% +8,8% +7,2% +72% +6,9% ae ieer\nne irene 69 N Pologne\nRE AuM (€m) 99 297 472 1,636 3,214 4,846 4,920 a oe 25. a a\nIpStuRR:3 Ukraine\nNumber of 6 13 17 32 52 71 62 =RE-=)Seti\nassets Be — Be\nEr \\ ie ‘ulgarie\nRE Leverage 0% 234% 296% 307% 358% 378% 347% Th a Ra Re\nTurquie\nsource ‘GD :::: as of December 2018\nPast performance is not a guarantee of future results\nSESE\n23 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 21,
|
||||
"text": "Our ESG approach: a defined framework to reach the best\nstandards in terms of low carbon commitment\n| knaronmemal and ch! aertormaren\nGED will integrate additional investment criteria in order to be a green\nfund.\nAs such, following preselection of the assets, the fund managers will Laci\nexclude assets ranked below D, and build a portfolio with a global\nranking above or equal to C. rst\nOn top of this ranking, we will perform a specific analysis on each asset ne lese,\nin order to be fully aware of its impact in environmental terms:\n- The carbon footprint ofthe assets\n- The 2 degrees trajectory of the assets ‚=\n- The exposure ofthe assets to climate risks 3000 5 G\n} #\nIn line with the Paris Agreement — COP 21* and the European directive** objectives for foreign\nassets, we will assess for each asset the greenhouse gas emissions reductions to be\nachieved and implement an energy consumption reduction trajectory, delivering to our\nclients a genuine low carbon approach backed by concrete analyses and reporting\n“the Paris Agreement:\n- limiting the average increase in the planet’s temperature to less than 2°C compared to pre-industrial levels\n- reinforcing capacity for adaptation to the harmful effects of climate change and promoting resilience to these changes\n**the European Directive :\n- The European Council has adopted an indicative objective to reducing energy consumption by 27% by 2030\nSESE\n24 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 22,
|
||||
"text": "A robust and balanced setup for efficient teamwork\nReal Estate\n(125 people)\nInstitutional Solutions\nReal Estate team\nInvestment teams\n(Research, Acquisitions\n& Sales, Asset\n| Advisory\nManagement)\nOperations (Fund\nControlling, Liability\nManagement... )\nMngt co.\nLuxembourg\n(76 people)\n| Ptf and Risk\n: Mngt Strong and long dated\nexperience as an AIFM\nGeneral Partner GP Sarl 4 people dedicated to\nLux real estate: Conducting\nOfficer, Portfolio\nManagement, Fund\nControlling and\ndedicated\nRisk&Compliance\nOfficer\n25 a. :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 23,
|
||||
"text": "Investment Case & Views\n28 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 24,
|
||||
"text": "Market\nViews\nEurope is a liquid, deep and attractive real estate market\n; Spread between office prime yield and 10-\nInvestment volumes in European real estate RR a. |\nyear Govies (in basis points, since 2000)\n300 Md € 4 r 6% 600 pb\n250 Md € 4 | 5% : 4\nS\n0\nU\n0\nR\np\nph\nb FE\nma€ x\n200 Md € + L 4% 300 pb all.\n200 pb a eo a ce i\n150 Md€ 4 | 3% 100 pb 4 TT =\n= ao . j | o am 0 pb min\n-100 pb —\n50 Md € + L 1% -200 pb\nO Md € + 0% -300 pb \" & = m P\n19 11 12 13 14 18 16 17 18 19 FS F KC Kw K & KS N!\nGa Q3\nPrcS\nme 2\nma CO! — Q2 2019 — Average 200— 0Q2 2019 —- (04 2018\nwm Average S1 2010 - 2019\n—EU 15 weighted prime rate (right scale) “End of period\n- Europe is a key destination for capital markets - Real Estate investors follow their acquisitions at historically\nlow rates, this behavior is led by office rents increases\n- European investment markets have been very active over the recent anticipations (and to some extent for the logistics), at low\nyears. Offices are the main asset class, with a little less than 1 euro financing rates, and a gap with 10 years rates significantly\nover 2 euros invested. higher than the long period average.\n- Local European actors and even often local national investors have - The spread between government bonds and prime yields is\ndominated the RE investment markets in Europe, but international still currently significantly high on many markets in a lasting\ninvestors are gaining market shares (mainly from Asia, Americas and low rates environment\nthe Middle East) .\n29 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 25,
|
||||
"text": "Market\nViews\nOffices are by far the main real estate asset class in Europe\nview of cities’ office prime headline\nOffice take-up and vacancy rate\nrents — Q2 2019\n14Mm? - - 12%\nAmsterdam Prague\nBarcelone\n12Mm? + + 10% Hambourg\nBruxelles\nFrancfort\nMilan\n10 Mm? 4 6.0 8%\nDublin\nLondres Paris\n8 Mm? 4 + 6% Varsovie\n6M mi» | 4% Berlin\nMunich\n4M mi - 1 2%\n2Mm? + + 0%\n0OMm?« -2% A d c e c c e li l n e e r ation of rents S de l c o l w i d ne o wn of rents A i c n c c e r l e e a r s a e t ion ofrents i S n l c o r w e d a o se wn of rents\n10 11 12 13 14 15 16 17 18 19\n(4\nma3\nRents variation intensity: weak moderate strong\nme)\nu() |\nu Average S1 2010-2019\n— Vacancy rate EU 15 {right scale)\newes Headline prime rent rental growth EU 15 (rught scale)\n- The office market has been particularly active at the 1° semester - Major EU markets should benefit from tenants demand\n2019, a performance to be highlighted in a context of economic\n- All markets have different rental cycles in terms speed: Pan\nslowdown and of high uncertainties: office commercialization have\nEuropean diversification will allow to anchor RE investment\nincreased over 1 year in Western Europe and are higher than the\nperformance\ndecade average.\nNB\n- Alot of companies continue to favour central zones for “talents\n- The positions are purely indicative and are not an investment recommendation or\nhunting” recruitment purposes., but they face a quality offer that is solicitation\n- City positions can move at different speeds and directions depending on various\nregularly lacking. This context of rarity, if it benefits “In white”\nparameters\nlaunches, it exerts upward pressure on facial rents.\nSource!\n30 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 26,
|
||||
"text": "A strategy for Europe today: stability & diversity\nCurrent Fund Target Allocation across Europe\nOur strategy for Continental Europe today: BE Conviction - Strong\n> targeting sustainable LT yield, value protection, with a ME Conviction - Medium\nhigh level of diversification and valuation potential FE Conviction - Low\nFrance Germany Benelux Other € Non € Target Themes\n/ Austria\nTargeting demand-driven markets featuring rent recovery or rent\nOffices 70% pressures and >200bps risk premiums over risk free rates, always\nin prime locations\nFocus on leases featuring fixed or floored rents with established\nHospitality\n= + + + = operators in markets where constrain of new offer exist. Risk\nHotels, others 10% premium must reflect any operational risk taken\nindustrial / Look at the opportunities in close to city centers distribution\n\\Warahöus + of. + = = 10% platforms while large modern logistic platforms might be out of\na e reach (high individual values).\nRetail (High Focus on prime high street retail or small central urban shopping\nstreet; retail + + + + - 10% centers in main secondary cities demonstrating positive data in\npark) terms of demographics and spending potential\nTier 1 (FR, UK, DE, BE, NL, LU, Nordics, SP, IT, CH) target 80%\nTier 2 target 20%\n31 GE : ::: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 27,
|
||||
"text": "Current views accross Europe\nThose markets are diverse and present perspectives and positions in Real BM Conviction - Strong\nEstate cycles which are highly dependent upon each local economic situations, BM Conviction - Medium\nperspectives and exceptional events affecting them:\n— Conviction - Low\na|\nGermany France Portugal\nPrime office assets in Prime markets are very Paris region is a deep and liquid market. Rents Lisbon is a small market but it experienced a\npricey unless rent reversion is real. Risk have some potential to improve. Considering rapid economic recovery in recent years and is\npremium remains attractive on a leveraged current low yield and fierce competition, office interesting for Core Offices, quality Retail asset\nbasis. Managteo core or build to core can make right outside CBD for Core + assets can be or Hotel walls with top operators. Limited\nsense as a LT investor in main cities. considered. Manage to core strategies could liquidity of this market means investment must\nResidential is also attractive make sense. be small\n||]\nBenelux Austria United Kingdom\nBrussels is an interesting market despite its high Office retail and hotel marketsto be looked at London office market attractiveness has\ndependency on EU commissions offices. as risk premium remains attractive and financing been hurt by the uncertainties introduced\nOpportunities are rare but worth looking at. offer as good conditions as in Germany. We since Brexit vote. Although presenting new\nDespite a recent decrease in yields, Amsterdam must remain cautious in this market where opportunities, the UK market does not\nand Luxemburg remain attractive for office and competing future supply can break present present today the best investment set.\nresidential equilibrium.\nIreland Finland Italy\nThe market is narrow but opportunities can Although rather small market, Finish office, retail Office market is over priced and leverage is not\nbe looked at in the Dublin office market, and hotel assets should be looked at. Asset size efficient. Each of Milan and Rome office\nwhich can benefit from Brexit. A particular should remain reasonable as this market lacks markets are narrow. competition is currently too\nfocus on the competitive future supply will be liquidity. Residential market can also be looked strong for prime assets. Focus could be made\nneeded. at although local investors present strong on retail in 2nd tier cities for best in class micro\ncompetition locations.\nPoland Spain Czech Rep\nPrime office market in Warsaw will be searched Madrid office and retail assets are interesting Office assets could be looked at on an\nfor opportunities as sellers (investors and while financing costs continue to decrease and opportunistic basis in Prague but this market is\ndevelopers) start to be reasonable in their economy slowly recovers, offering potential for now very competitive and can sometime be\nselling price. Investment should focus on Euro higher rents. Assets in prime locations (Madrid overpriced especially for Euro-denominated\ndenominated assets offering mainly Euro and Barcelona) should be favored as they have deals.\nrevenues. best potential for rent evolutions.\nSESE\n32 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 28,
|
||||
"text": "Appendices\nFocus on our ESG approach\nFund Information / Reportings\nRecent acquisitions in Europe\nBiographies\n33 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 29,
|
||||
"text": "Focus on the ESG approach\nA) The environmental and social mapping\nPerformance environnementale et sociale\nTo realize this mapping, we mainly use the\n71 / 100\nBREEAM-In-Use part 1 frame of\nG F E D C B A reference.\nee DE\n0 14 29 43 9 6 100\nThis internationally known frame of\nPollution 29% reference allows us to confirm the\n59% ~~. Santé et bien-étre\nrelevance of the realised analyses.\n52%\n100% OccupPatir =\nsol et écologie\nNous y dérogeons sur l’aspect énergie en\nDechets\n100% |\ns’interessant a l’annee de construction du\nFE ' Energie\nPerennite des \\ 65% bätiment et a la reglementation a laquelle\néquipements il etait soumis.\n78% Transport\nWe derogate from it on the energy aspect\n83%\nby focusing on the construction year of the\nbuilding and the rules and regulations to\nwhich it was subject.\n34 |"
|
||||
},
|
||||
{
|
||||
"page": 30,
|
||||
"text": "Focus on the ESG approach\nB) Energy-Carbon performances evolutions\nThis part is based on the consumption of the\nEvolution des performances Energie - Carbone\nasset. It allows to visualize the evolution of\nconsumption in relation to two objectives:\nEnergie Carbone\n(kWh,,/m?.an) | (kgCO,e/m?.an)\nPerformance de - Energetic objective: based on the\nréférence 286 17 reductions imposed by the tertiary decree in\n(2011)\nFrance and on recommended reductions by\nPerformance actuelle the European framework for energy and\n286 17\n(2011) climate for other European countries.\nObjectif 2030 171 14\n- Carbon objective: based on the necessary\nAvancement de reduction to ensure that the asset is on a\n100\nl'objectif (%) 0%\n[0 trajectory compatible with the Paris\nAgreement limiting the global warming to\n2 os\niy Les performances analysees prennent en compte :\n- les usages de l'immeuble (parties communes et/ou privatives)\n- le mix énergétique de chaque pays d'implantation (pour la The translation of the energetic performances\nconversion Energie-carbone)\nin carbon performances takes into account the\nLa performance carbone est issue des consommations\nenergetic mix of the asset's country.\nénergétiques uniquement (scope 1 et/ou 2).\n35 |"
|
||||
},
|
||||
{
|
||||
"page": 31,
|
||||
"text": "Focus on the ESG approach\nC) Exposure to climate risks\nRisques physiques lies au changement climatique\nLes risques physiques lies au changement climatique se traduisent par des evenements chroniques (élévation du niveau de la mer et de la\ntempérature) et exceptionnels (canicules, inondations, tempétes) pouvant endommager le batiment ou ses équipements.\nkz ow ee BER\nLe batiment est situé dans un milieu\nurbain dense, avec un phénoméne d’ilot\ni de chaleur urbain lors de fortes chaleurs\npouvant entrainer des appels de\npuissance supplémentaires en froid.\nHausse du Inondations dues Hausse de la Canicules Tempétes\nniveau de la mer aux pluies température moyenne\nThis part allows an evaluation of the exposure of the asset to 5 risks linked to climate changes. 3\ncriteria are considered to build this grade:\n- The geolocation of the asset and the resulting predictive scenarios of the climate change ;\n- The devices and characteristics of the asset allowing it to resist to these risks ;\n- The immediate environment of the asset that may include aggravating factors.\nre\n36 |"
|
||||
},
|
||||
{
|
||||
"page": 32,
|
||||
"text": "ESG\napproach\nFocus on our ESG approach: mobilization within our sector\nGERD Real Estate is one of the founding members of the Observatory of\nSustainable Real Estate, an independent and transparent forum for exchanges,\npromoting the sustainable development and innovation of French real estate. ®) B\npESOCIATing\nBATIMENT\nGHEE Real Estate is a member of the BBCA low carbon building association since\nBBCA\n2016.\nBAS CARBONE\nC,\nMEMBRE 2019\nGERD Real Estate participates in the working group on the creation of A SPIM ASSOCIATION FRANGAISI\nICIETES\nan SRI label applied to real estate. ACEMENT IMMOBILIET\nps\n33 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 33,
|
||||
"text": "Fund\nReporting\nFund Information Reporting\nFinancials : quarterly (D+45) Real Estate : semi-annually (D+90)\n- NAV calculation, consolidated accounts - Appraisal reports, global market review, asset\n- InREV adjustments, distributed dividends management report\nstatements (semi-annual), ratios =\nss | Be | == |e | ee\nGlobal synthesis : annually (D+90) Annual Report (D+120)\n- Annual accounts, market, financial and real estate - Annual audited accounts\nanalyses, updated business plan - Audit report\n— Be - - un\n-/— | naa\n3 =\n= 1\nWw oo Real Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 34,
|
||||
"text": "Recent deals in Europe\n(1/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nI [rem 00 Bo\nInvestment by unds\nINK MGallery Hotel Area: 148 rooms\nAmsterdam ! ee\nCore Tenant: Accor Hospitality Nedeland\nClosed in 2018 WAULT (years): 14\nAsset Value: €64,6m\nNet Initial Yield: 4.02%\nUnlevered CoC: 4.14%\nUnlevered IRR 10Y: 4.40%\nEl Portico Area: Office 20,814 sqm Investment by unds\nMadrid Parking 401 units\nCore Tenants: Multi tenants\nClosed in 2018 WAULB (years): 2,28\nPrice: €117,4m\nNet Initial Yield: 4.31%\nUnlevered CoC: 3.31%\nUnlevered IRR 10Y: 4.26%\n39 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 35,
|
||||
"text": "Examples of recent deals in Europe\n(2/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nroJmoe\nEnjoy Area: Office 16,970 sqm Forward sale deal,\nParis development to be\nCore Parking 64 units completed by end-2018.\nClosed in 2018 Tenant: AXA Services\nWAULT (years): 9 Co-investment between\nAsset Value: €258m net Fund and a French\nNet Initial Yield: 3.40% institutional investor\nUnlevered CoC: 3.30%\nUnlevered IRR 10Y: 3.30%\nArea: Office 28,564sqm Investment b¥@iiiFunds\nBBW Residential 2,494 sqm\nFranfurt Commercial 1,028 sqm\nCore Parking 347 units\nClosed in 2018 Mains tenants: KfW, Dwp Bank, Nomura\nWAULT (years): 10\nAsset Value: €141,2m\nNet Initial Yield: 4.25%\nUnlevered IRR 10Y: 3.60%\n40 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 36,
|
||||
"text": "Recent deals in Europe\n(3/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nDM [rom Terms\nGrand Central Area: Office 43,674 m? Forward sale deal,\nFrankfurt Storage 1,636 m? development to be\nCore Parking 783 units completed by end-2020.\nClosed in 2017 Tenant: Deutsche Bahn Netz AG (100%)\n-subsidiary of DB AG*\nWAULT (years): 20 GERD funds bought 100% of\nAsset Value: €324m net the deal and seek to share\nNet Initial Yield: 3.45% 50% of this deal with co-\nUn-levered Cash-on-cash: 3.48% investor(s).\nInvestor un-levered IRR 10Y: 3.22%\nRocket Tower Area: Office 26,192 m? Co-investment between\nBerlin Retail 1,765 m? GERuDnd and a Finnish\nCore Parking 411 units institutional investor\nClosed in 2017 Tenants: Multi (occupancy 96%)\nWALB (years): 13:5\nPrice: €149m net\nNet Initial Yield: 40%\nLTV: 45%\nCash-on-cash: 9.47%\nInvestor IRR 10Y: 7.58%\nmm \"DUB AG: responsible for railways maintenance\n41 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 37,
|
||||
"text": "Recent deals in Europe\n(4/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nECT [>777\nCoeur Défense Area: Office 182,765 m? Co-investment between\nParis, La Défense Main tenants: HSBC, RTE, Allianz and EDF EN and French\nCore WALB (years): 7 institutional investors\nClosed in 2017 Price: € 1,720m\nNet Initial Yield: 4,78%\nLTV: 52%\nCash-on-cash: 5.59%\nInvestor IRR 10Y: 7.46%\nTour Hekla Area: Office 79,876 m? Speculative development\nParis, La Défense Main tenant: Vacant Co-investment between\nCore WALB (years): and a French\nClosed in 2017 Price: € 582m institutional investor.\nNet Initial Yield: 6.7%\nLTV: 41%\nCash-on-cash: 5.8%\nInvestor IRR 10Y: 6.03%\n42 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 38,
|
||||
"text": "Recent deals in Europe\n(5/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nroens\nThe Atrium Area: Office 59,044 m? Co-investment wit»\nAmsterdam south-axis Parking 525 units investors\nCore Tenants: Multi (occupancy 68%)\nClosed in 2017 WALB (years): 8\nPrice: €920m net\nNet Initial Yield: 3.83%\nLTV: 60%\nCash-on-cash: 6-7%\nInvestor IRR 10Y: 7.57%\nThe Cloud Area: Office 23,807 m? Co-investment between\nAmsterdam Parking 195 units GERD0 ans GD\nCore Tenants: Multi (occupancy 98.4%) institutional investor\nClosed in 2017 WALB (years): 9.7\nPrice: €159m net\nNet Initial Yield: 4.24%\nLy: 40%\nCash-on-cash: 4.78%\nInvestor IRR 10Y: 5.56%\n43 GE: :: Estate Prime Europe"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
node_modules
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
Makefile
|
||||
helm-charts
|
||||
.env
|
||||
.editorconfig
|
||||
.idea
|
||||
coverage*
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"files.watcherExclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"files.readonlyInclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
FROM oven/bun:1 AS base
|
||||
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
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN bun run build
|
||||
|
||||
FROM nginx:1.28.0-alpine3.21
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=base /usr/src/app/dist /usr/share/nginx/html
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
Welcome to your new TanStack app!
|
||||
|
||||
# Getting Started
|
||||
|
||||
To run this application:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bunx --bun run start
|
||||
```
|
||||
|
||||
# Building For Production
|
||||
|
||||
To build this application for production:
|
||||
|
||||
```bash
|
||||
bunx --bun run build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
|
||||
|
||||
```bash
|
||||
bunx --bun run test
|
||||
```
|
||||
|
||||
## Linting & Formatting
|
||||
|
||||
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
|
||||
|
||||
|
||||
```bash
|
||||
bunx --bun run lint
|
||||
bunx --bun run format
|
||||
bunx --bun run check
|
||||
```
|
||||
|
||||
|
||||
## Routing
|
||||
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
|
||||
|
||||
### Adding A Route
|
||||
|
||||
To add a new route to your application just add another a new file in the `./src/routes` directory.
|
||||
|
||||
TanStack will automatically generate the content of the route file for you.
|
||||
|
||||
Now that you have two routes you can use a `Link` component to navigate between them.
|
||||
|
||||
### Adding Links
|
||||
|
||||
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
|
||||
|
||||
```tsx
|
||||
import { Link } from "@tanstack/react-router";
|
||||
```
|
||||
|
||||
Then anywhere in your JSX you can use it like so:
|
||||
|
||||
```tsx
|
||||
<Link to="/about">About</Link>
|
||||
```
|
||||
|
||||
This will create a link that will navigate to the `/about` route.
|
||||
|
||||
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
|
||||
|
||||
### Using A Layout
|
||||
|
||||
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.
|
||||
|
||||
Here is an example layout that includes a header:
|
||||
|
||||
```tsx
|
||||
import { Outlet, createRootRoute } from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
||||
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<>
|
||||
<header>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</nav>
|
||||
</header>
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
|
||||
|
||||
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
||||
|
||||
|
||||
## Data Fetching
|
||||
|
||||
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
const peopleRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/people",
|
||||
loader: async () => {
|
||||
const response = await fetch("https://swapi.dev/api/people");
|
||||
return response.json() as Promise<{
|
||||
results: {
|
||||
name: string;
|
||||
}[];
|
||||
}>;
|
||||
},
|
||||
component: () => {
|
||||
const data = peopleRoute.useLoaderData();
|
||||
return (
|
||||
<ul>
|
||||
{data.results.map((person) => (
|
||||
<li key={person.name}>{person.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
|
||||
|
||||
### React-Query
|
||||
|
||||
React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.
|
||||
|
||||
First add your dependencies:
|
||||
|
||||
```bash
|
||||
bun install @tanstack/react-query @tanstack/react-query-devtools
|
||||
```
|
||||
|
||||
Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`.
|
||||
|
||||
```tsx
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
// ...
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// ...
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can also add TanStack Query Devtools to the root route (optional).
|
||||
|
||||
```tsx
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => (
|
||||
<>
|
||||
<Outlet />
|
||||
<ReactQueryDevtools buttonPosition="top-right" />
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Now you can use `useQuery` to fetch your data.
|
||||
|
||||
```tsx
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["people"],
|
||||
queryFn: () =>
|
||||
fetch("https://swapi.dev/api/people")
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.results as { name: string }[]),
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{data.map((person) => (
|
||||
<li key={person.name}>{person.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).
|
||||
|
||||
## State Management
|
||||
|
||||
Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.
|
||||
|
||||
First you need to add TanStack Store as a dependency:
|
||||
|
||||
```bash
|
||||
bun install @tanstack/store
|
||||
```
|
||||
|
||||
Now let's create a simple counter in the `src/App.tsx` file as a demonstration.
|
||||
|
||||
```tsx
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Store } from "@tanstack/store";
|
||||
import "./App.css";
|
||||
|
||||
const countStore = new Store(0);
|
||||
|
||||
function App() {
|
||||
const count = useStore(countStore);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => countStore.setState((n) => n + 1)}>
|
||||
Increment - {count}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.
|
||||
|
||||
Let's check this out by doubling the count using derived state.
|
||||
|
||||
```tsx
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Store, Derived } from "@tanstack/store";
|
||||
import "./App.css";
|
||||
|
||||
const countStore = new Store(0);
|
||||
|
||||
const doubledStore = new Derived({
|
||||
fn: () => countStore.state * 2,
|
||||
deps: [countStore],
|
||||
});
|
||||
doubledStore.mount();
|
||||
|
||||
function App() {
|
||||
const count = useStore(countStore);
|
||||
const doubledCount = useStore(doubledStore);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => countStore.setState((n) => n + 1)}>
|
||||
Increment - {count}
|
||||
</button>
|
||||
<div>Doubled - {doubledCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.
|
||||
|
||||
Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.
|
||||
|
||||
You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).
|
||||
|
||||
# Demo files
|
||||
|
||||
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
|
||||
|
||||
# Learn More
|
||||
|
||||
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["src/routeTree.gen.ts"],
|
||||
"include": ["src/*", ".vscode/*", "index.html", "vite.config.js"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,23 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Caching configuration for static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Always serve index.html for any request
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Error handling
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-tsrouter-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Create TanStack App - frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "vite build && tsc",
|
||||
"serve": "vite preview",
|
||||
"test": "vitest run",
|
||||
"format": "biome format --write",
|
||||
"lint": "biome lint",
|
||||
"check": "biome check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fontsource/roboto": "^5.2.5",
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@tanstack/react-query": "^5.66.5",
|
||||
"@tanstack/react-query-devtools": "^5.66.5",
|
||||
"@tanstack/react-router": "^1.114.3",
|
||||
"@tanstack/react-router-devtools": "^1.114.3",
|
||||
"@tanstack/router-plugin": "^1.114.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-material-file-upload": "^0.0.4",
|
||||
"react-pdf": "^9.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"jsdom": "^26.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.1.0",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "TanStack App",
|
||||
"name": "Create TanStack App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { useState } from 'react'
|
||||
import FileUpload from 'react-material-file-upload'
|
||||
import {Box, Button, IconButton, Paper} from '@mui/material'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
export default function UploadPage() {
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const fileTypes = ["pdf"];
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
height="100vh"
|
||||
bgcolor="white"
|
||||
>
|
||||
<Box
|
||||
width="100%"
|
||||
maxWidth="1300px"
|
||||
display="flex"
|
||||
justifyContent="flex-end"
|
||||
px={2}
|
||||
>
|
||||
<IconButton onClick={() => navigate({ to: '/config' })}>
|
||||
<SettingsIcon fontSize="large" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: 900,
|
||||
height: 500,
|
||||
backgroundColor: '#eeeeee',
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
margin: '0px',
|
||||
padding: '0px',
|
||||
'& .MuiBox-root': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}}>
|
||||
<FileUpload
|
||||
value={files}
|
||||
onChange={setFiles}
|
||||
accept={`.${fileTypes.join(', .')}`}
|
||||
title="Hier Dokument hinziehen"
|
||||
buttonText="Datei auswählen"
|
||||
sx={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: '0px',
|
||||
'& svg': {
|
||||
color: '#9e9e9e',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
border: 'none',
|
||||
},
|
||||
'& .MuiButton-root': {
|
||||
backgroundColor: '#9e9e9e',
|
||||
},
|
||||
'& .MuiTypography-root': {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 500,
|
||||
marginBottom: 1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
mt: 4,
|
||||
backgroundColor: '#383838',
|
||||
}}
|
||||
disabled={files.length === 0}
|
||||
onClick={() => alert('Kein Backend, aber Button klickbar')}
|
||||
>
|
||||
Kennzahlen extrahieren
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
export default function LayoutAddition() {
|
||||
return <ReactQueryDevtools buttonPosition="bottom-right" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export function getContext() {
|
||||
return {
|
||||
queryClient,
|
||||
};
|
||||
}
|
||||
|
||||
export function Provider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
import "@fontsource/roboto/300.css";
|
||||
import "@fontsource/roboto/400.css";
|
||||
import "@fontsource/roboto/500.css";
|
||||
import "@fontsource/roboto/700.css";
|
||||
|
||||
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
|
||||
|
||||
import { pdfjs } from "react-pdf";
|
||||
// Import the generated route tree
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
...TanStackQueryProvider.getContext(),
|
||||
},
|
||||
defaultPreload: "intent",
|
||||
scrollRestoration: true,
|
||||
defaultStructuralSharing: true,
|
||||
defaultPreloadStaleTime: 0,
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize PDF.js worker
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
"pdfjs-dist/build/pdf.worker.min.mjs",
|
||||
import.meta.url,
|
||||
).toString();
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: "light",
|
||||
},
|
||||
});
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById("app");
|
||||
if (rootElement && !rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<TanStackQueryProvider.Provider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</TanStackQueryProvider.Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as ConfigImport } from './routes/config'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const ConfigRoute = ConfigImport.update({
|
||||
id: '/config',
|
||||
path: '/config',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/config': {
|
||||
id: '/config'
|
||||
path: '/config'
|
||||
fullPath: '/config'
|
||||
preLoaderRoute: typeof ConfigImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/config': typeof ConfigRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/config': typeof ConfigRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexRoute
|
||||
'/config': typeof ConfigRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/config'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/config'
|
||||
id: '__root__' | '/' | '/config'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
ConfigRoute: typeof ConfigRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ConfigRoute: ConfigRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
"__root__": {
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/config"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/config": {
|
||||
"filePath": "config.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
ROUTE_MANIFEST_END */
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
|
||||
// import Header from "../components/Header";
|
||||
|
||||
import TanStackQueryLayout from "../integrations/tanstack-query/layout.tsx";
|
||||
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface MyRouterContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||
component: () => (
|
||||
<>
|
||||
{/* <Header /> */}
|
||||
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools />
|
||||
|
||||
<TanStackQueryLayout />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Box, Button, IconButton, Paper, Typography } from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/config")({
|
||||
component: ConfigPage,
|
||||
});
|
||||
|
||||
function ConfigPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
height="100vh"
|
||||
width="100vw"
|
||||
bgcolor="white"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
pt={3}
|
||||
>
|
||||
<Box
|
||||
width="100%"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
px={4}
|
||||
>
|
||||
<Box display="flex" alignItems="center">
|
||||
<IconButton onClick={() => navigate({ to: "/" })}>
|
||||
<ArrowBackIcon fontSize="large" sx={{ color: '#383838' }}/>
|
||||
</IconButton>
|
||||
<Typography variant="h5" fontWeight="bold" ml={3}>
|
||||
Konfiguration der Kennzahlen
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: "#383838",
|
||||
"&:hover": { backgroundColor: "#2e2e2e" },
|
||||
}}
|
||||
>
|
||||
Neue Kennzahl hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
width: "90%",
|
||||
maxWidth: 1100,
|
||||
height: 400,
|
||||
mt: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "#eeeeee",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography color="textSecondary">To-do: Table hierhin</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import UploadPage from "../components/UploadPage";
|
||||
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: App,
|
||||
});
|
||||
|
||||
function App() {
|
||||
return <UploadPage/>;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
|
@ -0,0 +1,64 @@
|
|||
#########################################################
|
||||
#Run: in Terminal -> streamlit run PyMuPdf_st.py
|
||||
#########################################################
|
||||
|
||||
import streamlit as st
|
||||
import fitz # PyMuPDF
|
||||
from PIL import Image, ImageDraw
|
||||
import io
|
||||
|
||||
st.title("🔍 PDF Kennzahlen-Finder")
|
||||
|
||||
# PDF hochladen
|
||||
uploaded_file = st.file_uploader("PDF hochladen", type="pdf")
|
||||
|
||||
# Suchwort eingeben
|
||||
suchwort = st.text_input("Suchwort (z. B. wie)", value="wie")
|
||||
|
||||
if uploaded_file and suchwort:
|
||||
# PDF öffnen
|
||||
pdf_bytes = uploaded_file.read()
|
||||
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||
|
||||
fundstellen = []
|
||||
|
||||
# Suche durch alle Seiten
|
||||
for page_num in range(len(doc)):
|
||||
page = doc.load_page(page_num)
|
||||
rects = page.search_for(suchwort)
|
||||
|
||||
for rect in rects:
|
||||
fundstellen.append({
|
||||
"seite": page_num,
|
||||
"rect": rect
|
||||
})
|
||||
|
||||
if fundstellen:
|
||||
st.success(f"🔎 {len(fundstellen)} Fundstelle(n) für „{suchwort}“ gefunden.")
|
||||
|
||||
# Auswahl der Fundstelle
|
||||
auswahl = st.selectbox(
|
||||
"Fundstelle auswählen:",
|
||||
[f"Seite {f['seite'] + 1}" for f in fundstellen]
|
||||
)
|
||||
|
||||
index = [f"Seite {f['seite'] + 1}" for f in fundstellen].index(auswahl)
|
||||
fund = fundstellen[index]
|
||||
seite = doc.load_page(fund["seite"])
|
||||
rect = fund["rect"]
|
||||
|
||||
# Seite als Bild rendern
|
||||
zoom = 2 # Qualität
|
||||
pix = seite.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
|
||||
# Markierung zeichnen
|
||||
draw = ImageDraw.Draw(img)
|
||||
scale = pix.width / seite.rect.width
|
||||
r = rect
|
||||
rect_scaled = [r.x0 * scale, r.y0 * scale, r.x1 * scale, r.y1 * scale]
|
||||
draw.rectangle(rect_scaled, outline="red", width=3)
|
||||
|
||||
st.image(img, caption=f"Markierte Fundstelle auf Seite {fund['seite'] + 1}")
|
||||
else:
|
||||
st.warning("Keine Fundstellen gefunden.")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
__pycache__
|
||||
.venv
|
||||
.roproject
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.13
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Any
|
||||
from openai import AzureOpenAI
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""Manages configuration and environment variables for the MCP client."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize configuration with environment variables."""
|
||||
self.load_env()
|
||||
self.api_key = os.getenv("LLM_API_KEY")
|
||||
|
||||
@staticmethod
|
||||
def load_env() -> None:
|
||||
"""Load environment variables from .env file."""
|
||||
load_dotenv()
|
||||
|
||||
@staticmethod
|
||||
def load_config(file_path: str) -> dict[str, Any]:
|
||||
"""Load server configuration from JSON file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the JSON configuration file.
|
||||
|
||||
Returns:
|
||||
Dict containing server configuration.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If configuration file doesn't exist.
|
||||
JSONDecodeError: If configuration file is invalid JSON.
|
||||
"""
|
||||
with open(file_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
@property
|
||||
def llm_api_key(self) -> str:
|
||||
"""Get the LLM API key.
|
||||
|
||||
Returns:
|
||||
The API key as a string.
|
||||
|
||||
Raises:
|
||||
ValueError: If the API key is not found in environment variables.
|
||||
"""
|
||||
if not self.api_key:
|
||||
raise ValueError("LLM_API_KEY not found in environment variables")
|
||||
return self.api_key
|
||||
|
||||
|
||||
class Server:
|
||||
"""Manages MCP server connections and tool execution."""
|
||||
|
||||
def __init__(self, name: str, config: dict[str, Any]) -> None:
|
||||
self.name: str = name
|
||||
self.config: dict[str, Any] = config
|
||||
self.stdio_context: Any | None = None
|
||||
self.session: ClientSession | None = None
|
||||
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
|
||||
self.exit_stack: AsyncExitStack = AsyncExitStack()
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize the server connection."""
|
||||
command = (
|
||||
shutil.which("npx")
|
||||
if self.config["command"] == "npx"
|
||||
else self.config["command"]
|
||||
)
|
||||
if command is None:
|
||||
raise ValueError("The command must be a valid string and cannot be None.")
|
||||
|
||||
server_params = StdioServerParameters(
|
||||
command=command,
|
||||
args=self.config["args"],
|
||||
env={**os.environ, **self.config["env"]}
|
||||
if self.config.get("env")
|
||||
else None,
|
||||
)
|
||||
try:
|
||||
stdio_transport = await self.exit_stack.enter_async_context(
|
||||
stdio_client(server_params)
|
||||
)
|
||||
read, write = stdio_transport
|
||||
session = await self.exit_stack.enter_async_context(
|
||||
ClientSession(read, write)
|
||||
)
|
||||
await session.initialize()
|
||||
self.session = session
|
||||
except Exception as e:
|
||||
logging.error(f"Error initializing server {self.name}: {e}")
|
||||
await self.cleanup()
|
||||
raise
|
||||
|
||||
async def list_tools(self) -> list[Any]:
|
||||
"""List available tools from the server.
|
||||
|
||||
Returns:
|
||||
A list of available tools.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the server is not initialized.
|
||||
"""
|
||||
if not self.session:
|
||||
raise RuntimeError(f"Server {self.name} not initialized")
|
||||
|
||||
tools_response = await self.session.list_tools()
|
||||
tools = []
|
||||
|
||||
for item in tools_response:
|
||||
if isinstance(item, tuple) and item[0] == "tools":
|
||||
tools.extend(
|
||||
Tool(tool.name, tool.description, tool.inputSchema)
|
||||
for tool in item[1]
|
||||
)
|
||||
|
||||
return tools
|
||||
|
||||
async def execute_tool(
|
||||
self,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
retries: int = 2,
|
||||
delay: float = 1.0,
|
||||
) -> Any:
|
||||
"""Execute a tool with retry mechanism.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to execute.
|
||||
arguments: Tool arguments.
|
||||
retries: Number of retry attempts.
|
||||
delay: Delay between retries in seconds.
|
||||
|
||||
Returns:
|
||||
Tool execution result.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If server is not initialized.
|
||||
Exception: If tool execution fails after all retries.
|
||||
"""
|
||||
if not self.session:
|
||||
raise RuntimeError(f"Server {self.name} not initialized")
|
||||
|
||||
attempt = 0
|
||||
while attempt < retries:
|
||||
try:
|
||||
logging.info(f"Executing {tool_name}...")
|
||||
result = await self.session.call_tool(tool_name, arguments)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
logging.warning(
|
||||
f"Error executing tool: {e}. Attempt {attempt} of {retries}."
|
||||
)
|
||||
if attempt < retries:
|
||||
logging.info(f"Retrying in {delay} seconds...")
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
logging.error("Max retries reached. Failing.")
|
||||
raise
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""Clean up server resources."""
|
||||
async with self._cleanup_lock:
|
||||
try:
|
||||
await self.exit_stack.aclose()
|
||||
self.session = None
|
||||
self.stdio_context = None
|
||||
except Exception as e:
|
||||
logging.error(f"Error during cleanup of server {self.name}: {e}")
|
||||
|
||||
|
||||
class Tool:
|
||||
"""Represents a tool with its properties and formatting."""
|
||||
|
||||
def __init__(
|
||||
self, name: str, description: str, input_schema: dict[str, Any]
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.description: str = description
|
||||
self.input_schema: dict[str, Any] = input_schema
|
||||
|
||||
def format_for_llm(self) -> str:
|
||||
"""Format tool information for LLM.
|
||||
|
||||
Returns:
|
||||
A formatted string describing the tool.
|
||||
"""
|
||||
args_desc = []
|
||||
if "properties" in self.input_schema:
|
||||
for param_name, param_info in self.input_schema["properties"].items():
|
||||
arg_desc = (
|
||||
f"- {param_name}: {param_info.get('description', 'No description')}"
|
||||
)
|
||||
if param_name in self.input_schema.get("required", []):
|
||||
arg_desc += " (required)"
|
||||
args_desc.append(arg_desc)
|
||||
|
||||
return f"""
|
||||
Tool: {self.name}
|
||||
Description: {self.description}
|
||||
Arguments:
|
||||
{chr(10).join(args_desc)}
|
||||
"""
|
||||
|
||||
|
||||
class LLMClient:
|
||||
"""Manages communication with the LLM provider."""
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
self.api_key: str = api_key
|
||||
|
||||
def get_response(self, messages: list[dict[str, str]]) -> str:
|
||||
"""Get a response from the LLM.
|
||||
|
||||
Args:
|
||||
messages: A list of message dictionaries.
|
||||
|
||||
Returns:
|
||||
The LLM's response as a string.
|
||||
|
||||
Raises:
|
||||
httpx.RequestError: If the request to the LLM fails.
|
||||
"""
|
||||
url = "https://ai.exxeta.com/api/v2/azure/openai"
|
||||
|
||||
# Convert messages to the correct format expected by the API
|
||||
formatted_messages = []
|
||||
for msg in messages:
|
||||
# print(msg)
|
||||
formatted_messages.append({
|
||||
"role": msg["role"],
|
||||
"content": msg["content"]
|
||||
})
|
||||
|
||||
client = AzureOpenAI(
|
||||
api_key=self.api_key,
|
||||
api_version="2023-07-01-preview",
|
||||
base_url=url
|
||||
)
|
||||
response = client.chat.completions.create(
|
||||
messages=formatted_messages,
|
||||
model="gpt-4o-mini",
|
||||
# response_format={"type": "json_object"}
|
||||
# temperature=0.7,
|
||||
# top_p=0.95,
|
||||
# frequency_penalty=0,
|
||||
# presence_penalty=0,
|
||||
# max_tokens=800,
|
||||
# stop="",
|
||||
# stream=False
|
||||
)
|
||||
if response.choices[0].message.content:
|
||||
# print("response: " + response.choices[0].message.content)
|
||||
return response.choices[0].message.content
|
||||
|
||||
print("No response from Azure OpenAI")
|
||||
|
||||
# url = "https://api.groq.com/openai/v1/chat/completions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
payload = {
|
||||
"messages": messages,
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 4096,
|
||||
"top_p": 1,
|
||||
"stream": False,
|
||||
"stop": None,
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
|
||||
except httpx.RequestError as e:
|
||||
error_message = f"Error getting LLM response: {str(e)}"
|
||||
logging.error(error_message)
|
||||
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
status_code = e.response.status_code
|
||||
logging.error(f"Status code: {status_code}")
|
||||
logging.error(f"Response details: {e.response.text}")
|
||||
|
||||
return (
|
||||
f"I encountered an error: {error_message}. "
|
||||
"Please try again or rephrase your request."
|
||||
)
|
||||
|
||||
|
||||
class ChatSession:
|
||||
"""Orchestrates the interaction between user, LLM, and tools."""
|
||||
|
||||
def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
|
||||
self.servers: list[Server] = servers
|
||||
self.llm_client: LLMClient = llm_client
|
||||
|
||||
async def cleanup_servers(self) -> None:
|
||||
"""Clean up all servers properly."""
|
||||
cleanup_tasks = [
|
||||
asyncio.create_task(server.cleanup()) for server in self.servers
|
||||
]
|
||||
if cleanup_tasks:
|
||||
try:
|
||||
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
||||
except Exception as e:
|
||||
logging.warning(f"Warning during final cleanup: {e}")
|
||||
|
||||
async def process_llm_response(self, llm_response: str) -> str:
|
||||
"""Process the LLM response and execute tools if needed.
|
||||
|
||||
Args:
|
||||
llm_response: The response from the LLM.
|
||||
|
||||
Returns:
|
||||
The result of tool execution or the original response.
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
tool_call = json.loads(llm_response)
|
||||
if "tool" in tool_call and "arguments" in tool_call:
|
||||
logging.info(f"Executing tool: {tool_call['tool']}")
|
||||
logging.info(f"With arguments: {tool_call['arguments']}")
|
||||
|
||||
for server in self.servers:
|
||||
tools = await server.list_tools()
|
||||
if any(tool.name == tool_call["tool"] for tool in tools):
|
||||
try:
|
||||
result = await server.execute_tool(
|
||||
tool_call["tool"], tool_call["arguments"]
|
||||
)
|
||||
|
||||
if isinstance(result, dict) and "progress" in result:
|
||||
progress = result["progress"]
|
||||
total = result["total"]
|
||||
percentage = (progress / total) * 100
|
||||
logging.info(
|
||||
f"Progress: {progress}/{total} "
|
||||
f"({percentage:.1f}%)"
|
||||
)
|
||||
|
||||
return f"Tool execution result: {result}"
|
||||
except Exception as e:
|
||||
error_msg = f"Error executing tool: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
return error_msg
|
||||
|
||||
return f"No server found with tool: {tool_call['tool']}"
|
||||
return llm_response
|
||||
except json.JSONDecodeError:
|
||||
return llm_response
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Main chat session handler."""
|
||||
try:
|
||||
for server in self.servers:
|
||||
try:
|
||||
await server.initialize()
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to initialize server: {e}")
|
||||
await self.cleanup_servers()
|
||||
return
|
||||
|
||||
all_tools = []
|
||||
for server in self.servers:
|
||||
tools = await server.list_tools()
|
||||
all_tools.extend(tools)
|
||||
|
||||
tools_description = "\n".join([tool.format_for_llm() for tool in all_tools])
|
||||
|
||||
# print("All tools:", tools_description)
|
||||
|
||||
system_message = (
|
||||
"You are a helpful assistant with access to these tools:\n\n"
|
||||
f"{tools_description}\n"
|
||||
"Choose the appropriate tool based on the user's question. "
|
||||
"If no tool is needed, reply directly.\n\n"
|
||||
"IMPORTANT: When you need to use a tool, you must ONLY respond with "
|
||||
"the exact JSON object format below, nothing else:\n"
|
||||
"{\n"
|
||||
' "tool": "tool-name",\n'
|
||||
' "arguments": {\n'
|
||||
' "argument-name": "value"\n'
|
||||
" }\n"
|
||||
"}\n\n"
|
||||
"After receiving a tool's response:\n"
|
||||
"1. Transform the raw data into a natural, conversational response\n"
|
||||
"2. Keep responses concise but informative\n"
|
||||
"3. Focus on the most relevant information\n"
|
||||
"4. Use appropriate context from the user's question\n"
|
||||
"5. Avoid simply repeating the raw data\n\n"
|
||||
"Please use only the tools that are explicitly defined above."
|
||||
|
||||
)
|
||||
|
||||
messages = [{"role": "system", "content": system_message}]
|
||||
messages.append({"role": "assistant", "content": "You have to extract data from pdf files and have different tools for extracting."
|
||||
"For each value there is only one correct answer, try to find it with the tools provided."})
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("You: ").strip().lower()
|
||||
if user_input in ["quit", "exit"]:
|
||||
logging.info("\nExiting...")
|
||||
break
|
||||
|
||||
if user_input == "clear":
|
||||
logging.info("\nClearing...")
|
||||
messages = [{"role": "system", "content": system_message}]
|
||||
continue
|
||||
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
llm_response = self.llm_client.get_response(messages)
|
||||
logging.info("\nAssistant: %s", llm_response)
|
||||
|
||||
result = await self.process_llm_response(llm_response)
|
||||
|
||||
while result != llm_response:
|
||||
messages.append({"role": "assistant", "content": llm_response})
|
||||
messages.append({"role": "system", "content": result})
|
||||
|
||||
llm_response = self.llm_client.get_response(messages)
|
||||
logging.info("\nAssistant: %s", llm_response)
|
||||
|
||||
result = await self.process_llm_response(llm_response)
|
||||
|
||||
# logging.info("\nFinal response: %s", final_response)
|
||||
# messages.append(
|
||||
# {"role": "assistant", "content": final_response}
|
||||
# )
|
||||
|
||||
# messages.append({"role": "assistant", "content": llm_response})
|
||||
# logging.info("\nFinal response: %s", llm_response)
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("\nExiting...")
|
||||
break
|
||||
|
||||
finally:
|
||||
await self.cleanup_servers()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Initialize and run the chat session."""
|
||||
config = Configuration()
|
||||
server_config = config.load_config("servers_config.json")
|
||||
servers = [
|
||||
Server(name, srv_config)
|
||||
for name, srv_config in server_config["mcpServers"].items()
|
||||
]
|
||||
llm_client = LLMClient(config.llm_api_key)
|
||||
chat_session = ChatSession(servers, llm_client)
|
||||
await chat_session.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
[project]
|
||||
name = "arc1-prototype"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"mcp[cli]>=1.7.1",
|
||||
"openai>=1.77.0",
|
||||
]
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# server.py
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
import random
|
||||
|
||||
# Create an MCP server
|
||||
mcp = FastMCP("Demo")
|
||||
|
||||
risikoProfile = ["Core/Core+, Core", "Value Add"]
|
||||
risikoProfileSpacy = ["Core/Core+, Core", "Value Add", "3.2", "e au uae"]
|
||||
|
||||
# Add an addition tool
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
@mcp.tool()
|
||||
def getFromSpaCy() -> list:
|
||||
"""Get data from SpaCy"""
|
||||
return [{"page":random.randint(1, 35), "value": random.choice(risikoProfileSpacy), "key": "Risiko"},
|
||||
{"page":random.randint(1, 35), "value": "Real Estate", "key": "FondName"}]
|
||||
|
||||
@mcp.tool()
|
||||
def getFromChatGPT() -> list:
|
||||
"""Get data from ChatGPT"""
|
||||
return [{"page":random.randint(1, 35), "value": random.choice(risikoProfile), "key": "Risiko"},
|
||||
{"page":random.randint(1, 35), "value": "Real False Name", "key": "FondName"}]
|
||||
|
||||
@mcp.tool()
|
||||
def checkSpacyResult() -> dict:
|
||||
"""This tool checks the result of SpaCy, ensuring it meets certain criteria."""
|
||||
return {"page":random.randint(1, 35), "value": random.choice(risikoProfile), "key": "Risiko"}
|
||||
|
||||
@mcp.tool()
|
||||
def getFromChatGPTSingle(value: str) -> dict:
|
||||
"""This tool get a single value from ChatGPT. You can use the value to specify for which key the value should calculated"""
|
||||
return {"page":random.randint(1, 35), "value": random.choice(risikoProfile), "key": value}
|
||||
|
||||
context = ""
|
||||
|
||||
@mcp.tool()
|
||||
def getContext() -> str:
|
||||
"""This tool gets context information."""
|
||||
return context
|
||||
|
||||
@mcp.tool()
|
||||
def setContext(value: str) -> None:
|
||||
"""This tool sets context information."""
|
||||
global context
|
||||
context = value
|
||||
|
||||
# Add a dynamic greeting resource
|
||||
@mcp.resource("greeting://{name}")
|
||||
def get_greeting(name: str) -> str:
|
||||
"""Get a personalized greeting"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
""" Example prompt: Get data from spacy and exxeta and merge them. Validate if Core+ is a valid RISIKOPROFIL. """
|
||||
@mcp.tool()
|
||||
def validate_entity(entity: str, label: str) -> dict:
|
||||
"""Returns if the entity is valid based on hardcoded rules."""
|
||||
valid_risiko = {"core", "core+", "value-added", "opportunistisch"}
|
||||
normalized = entity.lower().replace(" ", "").replace("-", "")
|
||||
|
||||
if label.lower() == "risikoprofil" and normalized in valid_risiko:
|
||||
return {"status": "valid", "entity": entity}
|
||||
return {"status": "invalid", "entity": entity}
|
||||
|
||||
""" Example prompt: Get spacy and exxeta results and merge them. Then validate if "Core/Core+" is a valid Risikoprofil. """
|
||||
@mcp.tool()
|
||||
def merge_spacy_exxeta(spacy_result: list[dict], exxeta_result: list[dict]) -> list[dict]:
|
||||
"""Merge two results, mark as validated if label/entity/page match."""
|
||||
def norm(e): return e["entity"].lower().replace(" ", "")
|
||||
|
||||
merged = []
|
||||
seen = set()
|
||||
|
||||
for s in spacy_result:
|
||||
s_norm = norm(s)
|
||||
s_page = s["page"]
|
||||
match = next((e for e in exxeta_result if e["label"] == s["label"] and norm(e) == s_norm and e["page"] == s_page), None)
|
||||
if match:
|
||||
merged.append({**s, "status": "validated"})
|
||||
seen.add((match["entity"], match["page"]))
|
||||
else:
|
||||
merged.append({**s, "status": "spacy_only"})
|
||||
|
||||
for e in exxeta_result:
|
||||
if (e["entity"], e["page"]) not in seen:
|
||||
merged.append({**e, "status": "exxeta_only"})
|
||||
return merged
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"sqlite": {
|
||||
"command": "uv",
|
||||
"args": ["run", "mcp", "run", "server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload_time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload_time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc1-prototype"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp", extra = ["cli"] },
|
||||
{ name = "openai" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "mcp", extras = ["cli"], specifier = ">=1.7.1" },
|
||||
{ name = "openai", specifier = ">=1.77.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload_time = "2025-04-26T02:12:29.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload_time = "2025-04-26T02:12:27.662Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload_time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload_time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload_time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload_time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload_time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload_time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload_time = "2023-12-22T08:01:21.083Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload_time = "2023-12-22T08:01:19.89Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload_time = "2025-03-10T21:37:03.278Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/1b/4cd165c362e8f2f520fdb43245e2b414f42a255921248b4f8b9c8d871ff1/jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7", size = 308197, upload_time = "2025-03-10T21:36:03.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/aa/7a890dfe29c84c9a82064a9fe36079c7c0309c91b70c380dc138f9bea44a/jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b", size = 318160, upload_time = "2025-03-10T21:36:05.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/38/5888b43fc01102f733f085673c4f0be5a298f69808ec63de55051754e390/jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69", size = 341259, upload_time = "2025-03-10T21:36:06.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/5e/bbdbb63305bcc01006de683b6228cd061458b9b7bb9b8d9bc348a58e5dc2/jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103", size = 363730, upload_time = "2025-03-10T21:36:08.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/85/53a3edc616992fe4af6814c25f91ee3b1e22f7678e979b6ea82d3bc0667e/jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635", size = 405126, upload_time = "2025-03-10T21:36:10.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/b3/1ee26b12b2693bd3f0b71d3188e4e5d817b12e3c630a09e099e0a89e28fa/jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4", size = 393668, upload_time = "2025-03-10T21:36:12.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/87/e084ce261950c1861773ab534d49127d1517b629478304d328493f980791/jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d", size = 352350, upload_time = "2025-03-10T21:36:14.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/06/7dca84b04987e9df563610aa0bc154ea176e50358af532ab40ffb87434df/jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3", size = 384204, upload_time = "2025-03-10T21:36:15.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/2f/82e1c6020db72f397dd070eec0c85ebc4df7c88967bc86d3ce9864148f28/jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5", size = 520322, upload_time = "2025-03-10T21:36:17.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/fd/4f0cd3abe83ce208991ca61e7e5df915aa35b67f1c0633eb7cf2f2e88ec7/jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d", size = 512184, upload_time = "2025-03-10T21:36:18.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/3c/8a56f6d547731a0b4410a2d9d16bf39c861046f91f57c98f7cab3d2aa9ce/jiter-0.9.0-cp313-cp313-win32.whl", hash = "sha256:f7e6850991f3940f62d387ccfa54d1a92bd4bb9f89690b53aea36b4364bcab53", size = 206504, upload_time = "2025-03-10T21:36:19.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/1c/0c996fd90639acda75ed7fa698ee5fd7d80243057185dc2f63d4c1c9f6b9/jiter-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8ae3bf27cd1ac5e6e8b7a27487bf3ab5f82318211ec2e1346a5b058756361f7", size = 204943, upload_time = "2025-03-10T21:36:21.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/0f/77a63ca7aa5fed9a1b9135af57e190d905bcd3702b36aca46a01090d39ad/jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001", size = 317281, upload_time = "2025-03-10T21:36:22.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/39/a3a1571712c2bf6ec4c657f0d66da114a63a2e32b7e4eb8e0b83295ee034/jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a", size = 350273, upload_time = "2025-03-10T21:36:24.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/47/3729f00f35a696e68da15d64eb9283c330e776f3b5789bac7f2c0c4df209/jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf", size = 206867, upload_time = "2025-03-10T21:36:25.843Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload_time = "2023-06-03T06:41:14.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload_time = "2023-06-03T06:41:11.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ae/588691c45b38f4fbac07fa3d6d50cea44cc6b35d16ddfdf26e17a0467ab2/mcp-1.7.1.tar.gz", hash = "sha256:eb4f1f53bd717f75dda8a1416e00804b831a8f3c331e23447a03b78f04b43a6e", size = 230903, upload_time = "2025-05-02T17:01:56.403Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/79/fe0e20c3358997a80911af51bad927b5ea2f343ef95ab092b19c9cc48b59/mcp-1.7.1-py3-none-any.whl", hash = "sha256:f7e6108977db6d03418495426c7ace085ba2341b75197f8727f96f9cfd30057a", size = 100365, upload_time = "2025-05-02T17:01:54.674Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cli = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.77.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/c0/ea2e9a78bf88404b97e7b708f0823b4699ab2ee3f5564425b8531a890a43/openai-1.77.0.tar.gz", hash = "sha256:897969f927f0068b8091b4b041d1f8175bcf124f7ea31bab418bf720971223bc", size = 435778, upload_time = "2025-05-02T19:17:27.971Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/58/37ae3ca75936b824a0a5ca30491c968192007857319d6836764b548b9d9b/openai-1.77.0-py3-none-any.whl", hash = "sha256:07706e91eb71631234996989a8ea991d5ee56f0744ef694c961e0824d4f39218", size = 662031, upload_time = "2025-05-02T19:17:26.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload_time = "2025-04-29T20:38:55.02Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload_time = "2025-04-29T20:38:52.724Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload_time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload_time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload_time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload_time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload_time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload_time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload_time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload_time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload_time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload_time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload_time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload_time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload_time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload_time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload_time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload_time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload_time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload_time = "2025-04-23T18:32:25.088Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload_time = "2025-04-18T16:44:48.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload_time = "2025-04-18T16:44:46.617Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload_time = "2025-01-06T17:26:30.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload_time = "2025-01-06T17:26:25.553Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload_time = "2025-03-25T10:14:56.835Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload_time = "2025-03-25T10:14:55.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload_time = "2024-12-16T19:45:46.972Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload_time = "2025-03-30T14:15:14.23Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload_time = "2025-03-30T14:15:12.283Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload_time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/35/7d8d94eb0474352d55f60f80ebc30f7e59441a29e18886a6425f0bccd0d3/sse_starlette-2.3.3.tar.gz", hash = "sha256:fdd47c254aad42907cfd5c5b83e2282be15be6c51197bf1a9b70b8e990522072", size = 17499, upload_time = "2025-04-23T19:28:25.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/20/52fdb5ebb158294b0adb5662235dd396fc7e47aa31c293978d8d8942095a/sse_starlette-2.3.3-py3-none-any.whl", hash = "sha256:8b0a0ced04a329ff7341b01007580dd8cf71331cc21c0ccea677d500618da1e0", size = 10235, upload_time = "2025-04-23T19:28:24.115Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload_time = "2025-04-13T13:56:17.942Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload_time = "2025-04-13T13:56:16.21Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload_time = "2024-11-24T20:12:22.481Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload_time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641, upload_time = "2025-04-28T21:40:59.204Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload_time = "2025-04-28T21:40:56.269Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload_time = "2025-02-25T17:27:59.638Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload_time = "2025-02-25T17:27:57.754Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload_time = "2025-04-19T06:02:50.101Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload_time = "2025-04-19T06:02:48.42Z" },
|
||||
]
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.11.8
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# ARC2 Prototype Backend
|
||||
|
||||
### 1. Create and activate a virtual environment
|
||||
|
||||
<pre><code>pyenv shell 3.11.8
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
</code></pre>
|
||||
|
||||
### 2. Install dependencies
|
||||
|
||||
<pre><code>pip install -r requirements.txt
|
||||
</code></pre>
|
||||
|
||||
### 3. Run the Flask app
|
||||
|
||||
<pre><code>python app.py
|
||||
</code></pre>
|
||||
|
||||
Visit: <a href="http://127.0.0.1:5000">http://127.0.0.1:5000</a>
|
||||
|
||||
### 4. Upload a PDF
|
||||
|
||||
<pre><code>curl -X POST http://127.0.0.1:5000/upload \
|
||||
-F "file=@../../pitch-books/Pitchbook 1.pdf"
|
||||
</code></pre>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
from flask import Flask, request, jsonify
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from ocr_pdf_service.ocr_runner import run_ocr_and_extract
|
||||
from exxeta_service.exxeta_client import extract_with_exxeta
|
||||
from spacy_service.spacy_extractor import extract_with_spacy
|
||||
from merge_validate_service.validator import merge_and_validate_entities
|
||||
|
||||
app = Flask(__name__)
|
||||
UPLOAD_FOLDER = Path("pitchbooks")
|
||||
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return "Backend is running!"
|
||||
|
||||
@app.route("/upload", methods=["POST"])
|
||||
def upload():
|
||||
file = request.files.get("file")
|
||||
|
||||
if not file or file.filename == "":
|
||||
return jsonify({"error": "No file provided"}), 400
|
||||
|
||||
filepath = UPLOAD_FOLDER / file.filename
|
||||
file.save(filepath)
|
||||
|
||||
try:
|
||||
# Step 1: Run OCR
|
||||
ocr_result = run_ocr_and_extract(filepath)
|
||||
with open(ocr_result["json_path"], encoding="utf-8") as f:
|
||||
pitchbook_pages = json.load(f)
|
||||
|
||||
# Step 2: Extract with both engines
|
||||
extract_with_exxeta(pitchbook_pages)
|
||||
extract_with_spacy(pitchbook_pages)
|
||||
|
||||
# Step 3: Merge and validate results
|
||||
merge_and_validate_entities(filter_label="RISIKOPROFIL")
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
return "status: complete\n"
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
EXXETA_API_KEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbiI6IjIzYzA0NGEzOWY5OWIxMjdmODA5ODA0YmMxZTczN2UyIn0.uOD9GhvFl1hqd2B3dyb0IOJ4x_o1IPcMckeQxh2KNj0"
|
||||
EXXETA_BASE_URL = "https://ai.exxeta.com/api/v2/azure/openai"
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
from exxeta_service.config import EXXETA_API_KEY, EXXETA_BASE_URL
|
||||
import requests
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
MODEL = "gpt-35-turbo"
|
||||
OUTPUT_FOLDER = Path(__file__).resolve().parent / "output"
|
||||
OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def extract_with_exxeta(pages_json):
|
||||
results = []
|
||||
|
||||
for page_data in pages_json:
|
||||
page_num = page_data.get("page")
|
||||
text = page_data.get("text", "").strip()
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
prompt = (
|
||||
"Bitte extrahiere alle relevanten Nennungen von Risikoprofilen eines Fonds aus folgendem Pitchbook-Text.\n"
|
||||
"Nur Begriffe wie \"Core\", \"Core+\", \"Core/Core+\", \"Value-added\" oder \"Opportunistisch\" sowie sehr ähnliche Varianten extrahieren.\n\n"
|
||||
"Wenn mehrere dieser Begriffe direkt zusammen genannt werden, egal ob durch Kommas (\",\"), Schrägstriche (\"/\") oder Wörter wie \"und\" verbunden, "
|
||||
"bitte sie als **eine gemeinsame Entität** extrahieren, im Originalformat.\n\n"
|
||||
"Beispiele:\n"
|
||||
"- \"Core, Core+\" → entity: \"Core, Core+\"\n"
|
||||
"- \"Core/Core+\" → entity: \"Core/Core+\"\n"
|
||||
"- \"Core and Core+\" → entity: \"Core and Core+\"\n\n"
|
||||
"Gib die Ergebnisse als reines JSON-Array im folgenden Format aus:\n"
|
||||
f"[{{\"label\": \"RISIKOPROFIL\", \"entity\": \"Core, Core+\", \"page\": {page_num}}}]\n\n"
|
||||
"Falls keine Risikoprofile vorhanden sind, gib ein leeres Array [] zurück.\n\n"
|
||||
"Nur JSON-Antwort ohne Kommentare oder zusätzlichen Text.\n\n"
|
||||
"TEXT:\n" + text
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {EXXETA_API_KEY}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": "Du bist ein Finanzanalyst, der Fondsprofile auswertet. Antworte nur mit validen JSON-Arrays."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.0
|
||||
}
|
||||
|
||||
url = f"{EXXETA_BASE_URL}/deployments/{MODEL}/chat/completions"
|
||||
|
||||
for attempt in range(2):
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
content = response.json()["choices"][0]["message"]["content"]
|
||||
content = content.strip()
|
||||
|
||||
if content.startswith("```json"):
|
||||
content = content.split("```json")[1]
|
||||
if content.endswith("```"):
|
||||
content = content.split("```")[0]
|
||||
content = content.strip()
|
||||
|
||||
page_results = json.loads(content)
|
||||
|
||||
if page_results:
|
||||
results.extend(page_results)
|
||||
|
||||
break # Success
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed on page {page_num} (attempt {attempt+1}): {e}")
|
||||
|
||||
out_path = OUTPUT_FOLDER / f"exxeta-results.json"
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return results
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
[
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core and Core+",
|
||||
"page": 4
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core, core+, value-added",
|
||||
"page": 7
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 9
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core/Core+",
|
||||
"page": 10
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core / Core+",
|
||||
"page": 12
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core/Core+",
|
||||
"page": 19
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core/core+",
|
||||
"page": 20
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core/core+",
|
||||
"page": 20
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 28
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 26
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Value-added",
|
||||
"page": 26
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core Offices, Core + assets",
|
||||
"page": 27
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core, Core+",
|
||||
"page": 33
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 35
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 35
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core Parking",
|
||||
"page": 36
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core Parking",
|
||||
"page": 36
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 37
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 37
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 38
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 38
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,630 @@
|
|||
[
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 1,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core and Core+",
|
||||
"page": 4,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CITIES",
|
||||
"page": 6,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "closed-end and open-ended",
|
||||
"page": 6,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Club",
|
||||
"page": 6,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Club",
|
||||
"page": 7,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Club",
|
||||
"page": 7,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core, core+, value-added",
|
||||
"page": 7,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Each",
|
||||
"page": 8,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Land",
|
||||
"page": 8,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "C",
|
||||
"page": 8,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 9,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Fund\nFund Objective",
|
||||
"page": 10,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core/Core+",
|
||||
"page": 10,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core/core+",
|
||||
"page": 10,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core/Core+",
|
||||
"page": 10,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CH",
|
||||
"page": 10,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 10,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "June",
|
||||
"page": 11,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Lock",
|
||||
"page": 11,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "before",
|
||||
"page": 11,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core / Core +",
|
||||
"page": 12,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core\n/ core+",
|
||||
"page": 12,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Long",
|
||||
"page": 12,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Term / core+",
|
||||
"page": 12,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Hold",
|
||||
"page": 12,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 12,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core/core+",
|
||||
"page": 12,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Fund\nSees has",
|
||||
"page": 13,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Good New",
|
||||
"page": 14,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 14,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core 85m-90",
|
||||
"page": 14,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "June\n",
|
||||
"page": 14,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Good New",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Good",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Leed Platinium",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "City",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Good New",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core CBD",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 15,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Good New",
|
||||
"page": 15,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core/Core+",
|
||||
"page": 19,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core/core+",
|
||||
"page": 20,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core/core+",
|
||||
"page": 20,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "C.",
|
||||
"page": 21,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Fund",
|
||||
"page": 22,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Sarl",
|
||||
"page": 22,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Look",
|
||||
"page": 26,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "High",
|
||||
"page": 26,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Tier",
|
||||
"page": 26,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CH",
|
||||
"page": 26,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 26,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Value-added",
|
||||
"page": 26,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 27,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "core",
|
||||
"page": 27,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core +",
|
||||
"page": 27,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Each",
|
||||
"page": 27,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core Offices, Core + assets",
|
||||
"page": 27,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 28,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "kgCO,e/m?.an",
|
||||
"page": 30,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "C",
|
||||
"page": 31,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CARBONE\nC,\n",
|
||||
"page": 32,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Fund\n",
|
||||
"page": 33,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "semi-annually",
|
||||
"page": 33,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core, Core+",
|
||||
"page": 33,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 34,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CoC",
|
||||
"page": 34,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 34,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CoC",
|
||||
"page": 34,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "CoC",
|
||||
"page": 35,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core Parking",
|
||||
"page": 35,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 35,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 35,
|
||||
"status": "single-source",
|
||||
"source": "exxeta"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "3/5",
|
||||
"page": 36,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 36,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Cash-on-cash",
|
||||
"page": 36,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core Parking",
|
||||
"page": 36,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Cash-on-cash",
|
||||
"page": 36,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 37,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Cash-on-cash",
|
||||
"page": 37,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 37,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Cash-on-cash",
|
||||
"page": 37,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "5/5",
|
||||
"page": 38,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 38,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Cash-on-cash",
|
||||
"page": 38,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Core",
|
||||
"page": 38,
|
||||
"status": "validated"
|
||||
},
|
||||
{
|
||||
"label": "RISIKOPROFIL",
|
||||
"entity": "Cash-on-cash",
|
||||
"page": 38,
|
||||
"status": "single-source",
|
||||
"source": "spacy"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
from pathlib import Path
|
||||
import json
|
||||
|
||||
def normalize_entity(entity_str):
|
||||
return ''.join(entity_str.replace('\n', ' ').lower().split()) if entity_str else ""
|
||||
|
||||
def load_json(path: Path):
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def merge_and_validate_entities(filter_label=None):
|
||||
base = Path(__file__).resolve().parent.parent
|
||||
spacy_path = base / "spacy_service/output/spacy-results.json"
|
||||
exxeta_path = base / "exxeta_service/output/exxeta-results.json"
|
||||
output_path = base / "merge_validate_service/output/merged-results.json"
|
||||
|
||||
spacy_data = load_json(spacy_path)
|
||||
exxeta_data = load_json(exxeta_path)
|
||||
|
||||
merged = []
|
||||
seen = set()
|
||||
|
||||
for s in spacy_data:
|
||||
s_norm = normalize_entity(s["entity"])
|
||||
s_page = s["page"]
|
||||
|
||||
match = next(
|
||||
(e for e in exxeta_data
|
||||
if e["label"] == s["label"] and
|
||||
normalize_entity(e["entity"]) == s_norm and
|
||||
e["page"] == s_page),
|
||||
None
|
||||
)
|
||||
|
||||
if match:
|
||||
merged.append({**s, "status": "validated"})
|
||||
seen.add((match["entity"], match["page"]))
|
||||
else:
|
||||
merged.append({**s, "status": "single-source", "source": "spacy"})
|
||||
|
||||
for e in exxeta_data:
|
||||
if (e["entity"], e["page"]) not in seen:
|
||||
merged.append({**e, "status": "single-source", "source": "exxeta"})
|
||||
|
||||
merged.sort(key=lambda x: (x.get("page", 0), x.get("label", "")))
|
||||
|
||||
if filter_label:
|
||||
merged = [m for m in merged if m.get("label") == filter_label]
|
||||
|
||||
with output_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(merged, f, indent=2)
|
||||
return merged
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import subprocess
|
||||
from pathlib import Path
|
||||
import pdfplumber
|
||||
import json
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
OUTPUT_FOLDER = BASE_DIR / "output"
|
||||
OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def run_ocr_and_extract(pdf_path: str):
|
||||
pdf_path = Path(pdf_path)
|
||||
output_pdf = OUTPUT_FOLDER / f"pitchbook-OCR.pdf"
|
||||
json_path = OUTPUT_FOLDER / f"text-per-page.json"
|
||||
|
||||
# Call ocrmypdf
|
||||
cmd = [
|
||||
"ocrmypdf",
|
||||
"--force-ocr",
|
||||
"--output-type", "pdfa",
|
||||
"--language", "deu+eng",
|
||||
str(pdf_path),
|
||||
str(output_pdf)
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"OCR failed:\n{result.stderr.decode()}")
|
||||
|
||||
with pdfplumber.open(output_pdf) as pdf:
|
||||
pages = [{"page": i + 1, "text": (page.extract_text() or "").strip()} for i, page in enumerate(pdf.pages)]
|
||||
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(pages, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {
|
||||
"ocr_pdf": str(output_pdf),
|
||||
"json_path": str(json_path)
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,154 @@
|
|||
[
|
||||
{
|
||||
"page": 1,
|
||||
"text": "Real Estate Prime Europe\nAccess the Core of European Prime Cities with a green SRI fund\nincluding a genuine low carbon commitment\nFor professional investors only. Not for further distribution. Information is valid as of December 2018"
|
||||
},
|
||||
{
|
||||
"page": 2,
|
||||
"text": "Content\n1. Executive Summary\n. GRD Real Estate\nThe Fund\nExpertise & Investment Process\nInvestment Case & Views\n»wp\nao\nAppendices\nFund Investment Process\nos Detailed SRI Process\n— Reporting\n— Biographies\n2 See : ::: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 3,
|
||||
"text": "01\nExecutive summary\n3 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 4,
|
||||
"text": "Executive summary\nA selective pan European investment strategy\n- European economics in most countries should sustain Office leasing\nactivity, retail consumption and business Hotels. Considering the\ndepth of these market segments, selectivity will be key.\n- A strategy targeting high quality Core and Core+ buildings, with\ndefined SRI objectives, in order to extract value through an active\nasset management.\nWhat makes us different?\n- Se, ss: long established real estate investment\nand fund management player in Europe, allowing for a clear view of\nopportunities coming to the market (€4bn of acquisitions out of\nFrance over the past 3 years).\n- GRD semi-open architecture based on a strong integrated\nplatform in France, Luxembourg and Italy and long standing\npartnerships in major European countries gives us the required\nlocal historical expertise and the important flexibility to build and\ndiligently manage a well balanced pan European portfolio.\n- GRDas a leading player in ESG expertise is putting all\n| resources to structure products in line with our ambition: AREPE will\na be a low carbon fund compliant with the Qi Real Estate\nsustainable investment Charter.\n4 GE: :: Estate Prime Europe EEE\n|—________|”|\"_)"
|
||||
},
|
||||
{
|
||||
"page": 5,
|
||||
"text": "02\nSERes|i Estate"
|
||||
},
|
||||
{
|
||||
"page": 6,
|
||||
"text": "A leader in European real estate\nA LEADER IN EUROPEAN PRIME CITIES €33 bn 125 750\nOf AuM Dedicated people Properties in Europe\nGREED Real Estate is a company specialized in developing, i.\nstructuring and managing European focused property funds. Gr: — = een\nThanks to the power of its inflows carried out the largest\ntransactions in the European market, with €4bn of acquisitions out of\nFrance over the past 3 years.\n- WEB sources assets across Europe, structures the acquisitions\nand their financing, and manages all type of properties with a focus on\nOffices. 700+ properties in France, Italy, Germany, the Netherlands,\nUK, Czech Republic, Luxembourg...\nACOMPLETE OFFERING\nSee the map with detail for each of the 750 assets on:\n- Commingled Funds (closed-end and open-ended); Dedicated Funds; Club Deals\n& Joint Ventures ; Mandates (tailor made solution).\n- A leading player in managing and structuring regulated funds in France. mOffice 75,6%\nBRetail 13,8%\n- A window for international clients looking to access the European real estate\nEB Hotels 4,3%\nmarket for diversification\n\" Industrial/logistics 1,0%\n\" Residentials 9.4%\nOthers 4,9%\nove > 201:\n6 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 7,
|
||||
"text": "Comprehensive service offering to institutions\nDirect Investment Indirect Investment\nDedicated\nClub Deals Mandates Funds Commingled Funds\nClub Deals: investing with similar-minded investors Different strategies (single country or pan-European, single\nasset class or diversified)\nMandates: build from a tailor made proposal, defined according ; ;\nto the client’s constraints & guidelines Different risk profile (core, core+, value-added)\nDedicated funds: build a structure to manage client’s assets Different leverage levels\nDifferent jurisdictions (French OPCI, Lux. SIFs, etc.)\nSESE\nT GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 8,
|
||||
"text": "SEBeal Estate : a genuine low carbon commitment\nA fully documented sustainable investment charter\nEnvironmental and social performance\nEach of our assets is ranked by our dedicated team, from A to G.\nG F E D C B A\nWe use a well-known referential to perform a thorough analysis of 0 14 29 43 57 86 100\neach asset: the BREEAM In Use Part 1 international referential is fully Pollution\n59% Health & wellbeing\nexploited in order to perform an analysis of the environmental and social 52%\nperformance of the assets. Land we\n100%\nWaste\n100%\nEnergy\nGERD::| Estate has also developed additional and specific tools Materials 65%\n51%\nto complete and strengthen our sustainable approach on an asset by Water\n78% Transport\nasset basis: 23%\nThe Carbon Footprint 2°C Trajectory Climate Risk\nThis footprint is calculated based on: This trajectory will help assess the This evaluation shows the exposure of\nEnergy and leaks of refrigerants (scope 1) greenhouse gas emissions’ reductions the asset to different climate related risks\nElectricity, water and energy consumption needed to respect the Paris agreement (sea level, floods, temperature,\n(scope 2) heatwaves, storms...)\nMaterials used for construction or\nrefurbishment (Scope 3)\nGED Real Estate has built a genuine low carbon approach, in line with the best\nstandards\n8 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 9,
|
||||
"text": "The Fund\nSE Real Estate Prime Europe\n9 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 10,
|
||||
"text": "The Fund\nFund Objective\nNavigate the diversity of the Core/Core+ investment opportunities\nin European Prime Cities\nGEDis an open-ended Lux-based fund providing an attractive core/core+ real estate exposure, leveraging\nGRRE expertise in European RE markets. It offers diversification in terms of pan-European geographies and\nsectors: Offices, Retail and Hotels.\n- Risk level: Core/Core+\n- Type: essentially new or recently refurbished assets\nStrategy\n- Individual asset size: ranging from € SOM to € 200M\ncharacteristics\n- Leverage: 50% max (at asset and fund level) / Target 40-45%\n- A low carbon approach fully compliant with the best standards\n- Sector: offices (70% target; 60% min), all other types (30% target; 40% max)\n- Geography: 100% Europe\n- Tier 1: Min 75% in total, Max 40% by country (FR, UK, DE, BE, NL, LU, Nordics,\nAllocation SP, IT, CH)\nGuidelines - Tier 2: Max 25% in total in rest of Europe, max 10% by country\n- Non Euro investments: max 20%\n- Manage to Core: max 20%\n- Concentration limits: max 25% by asset\nTarget Returns - IRR: 6% - 7%\n(net of fees and tax) - Cash on Cash: 4% - 5%\nSt\n10 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 11,
|
||||
"text": "The caine\n4aodPr.\nFund Key Features\nA Luxembourg Alternative Investment Fund\ndenominated in EUR\nTarget Size: Target equity of€ 500m (€1 Bn of assets)\nRegulatory Qualification: Luxembourg AIF\nFund Structure “ Legal Form: Luxembourg Limited Partnership\nCurrency: EUR\nMinimum Investment: €5m\nSubscriptions: Quarterly. Queue system, with pari passu calls to the oldest vintage.\nRedemptions: Every semester (Dec and June). Queue system by vintage after lock up period.\nLock up: 5 years for investors entering in 2019, 4 years for investors entering in 2020 then 3\nyears\nLiquidity - Gates: 10% of NAV per annum\nRedemption deadline: 18 months after demand\nRedemption fee: If there are sufficient inflows when redemptions are outstanding, no fee on\nredemption ; otherwise, redemption fee will be 3% (10Y holding period), 2% (15Y) or 1% (>15Y)\nof NAV\nAsset Management/ Fund Management: sliding scale considering committed amount\n55-50-40bp x NAV (5-10; 10-50; 50+ ME). Fee only payable on investment called.\nFees For investors committing before 31/12/2019: Rebate of 10bp for 5 years after subscription\n(before VAT) = Acquisition: 0.5% to 1% x GAV following asset size\nDisposal: 200k€, flat\nPerformance fee: 20% above 7% IRR (payable by investors on realised profit)\n11 GE : :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 12,
|
||||
"text": "The Fund\nWhy a Core / Core + investment program?\nBenefits of the core\n/ core+ segment\n- Capital preservation*\n- Room to lock in Risk premia over\nfinancing rates\nDrawbacks ofthe core\n- Assets attractive for Long Term / core+ segment\nfinancing\n- Strong competition on this segment of\n- Assets adapted to a Buy and Hold the market for investment\nstrategy\n- Real Estate yields testing historically\n- Assets that generate running yields low levels\n- Adeep market improving availability\n- Core assets leave less room for active\nand asset liquidity\nasset management value creation\n- Better resilience to potential interest\nrate hikes (which usually triggers flight\nto quality)\n*capital preservation is defined here as a characteristic of core/core+ investments. There is no guarantee of capital.\nSESE\n12 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 13,
|
||||
"text": "The Fund\nSees has a strong track record versus the MSCI PEPFI*\n% 2015 2016 2017 2018 3Y 3Y (unl.) M S C Pas\nGHEE Total return 5,3 16,1 13,6 8,9 12,8 8,0 =\nLTV 71,0 62,4 58,9 58,2 59,8 0,0\nPercentile 80th 5th 5th 10th 5th\nPEPFI Total return 8,2 5,7 6,4 6,4 6,2 5,3\nLTV 20,6 21,5 21,9 21,8 21,7 0,0\nComments\n° LTV: part of the performance is down to leverage and ap ; significantly more leveraged than the benchmark and will remain so even\nafter we have reduced leverage to ca. 45%. Given the low cost of debt today, we believe this level of leverage makes sense: it boost\nincome return while the core nature of the portfolio should dampen a capital loss in case of a market downturn.\n. Asset-mix: QD current allocation is 100% Germany and a mixture of retail/office/hotel. As such, its asset and country allocation is less\nrisky than the benchmark which includes more risky countries and asset types (eg industrial/logistics).\n*PEPFI: Pan European Property Fund index QT: state as at end of December 2018\n16 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 14,
|
||||
"text": "Indicative pipeline of Investments — June 2019\nTechnical\nCountry / city Sector Risk Deal size Location Tenancy Comments\nspecificities\nOffice building 500m away from the European\nBELGIUM Offices Core Excellent New building Vacant Commission - well served by the subway (250m away\n80m\nBrussels Delivery Q4 2020 from one of the major metro station - 67 parking spaces -\nDelivery Q4 2020\nMulti let office building in the Sought after brussel's\nBELGIUM Fully let\nOffices Core 40m Good Recent building Euopean District - 100% let to 8 tenants - WALB 5,42\nBrussels WALB 5,42\nyears - Share deal - NIY around 4,17%\nFully let Office building in the South of Madrid - - 7 years lease\nSPAIN Completely\nOffices Core <50m Good Single tenant recently signed by an energy company - 47 parking\nMadrid refurbished\nspaces - Built in 1891 refurbished in 2019 -\nFully let\nFRANCE New building\nOffices Core 400m Good Very well located — LT lease - very good tenants\nLevallois Single tenant\nFRANCE New building Fully let\nOffices Core 300m Good New building in Paris\nParis 14 Multi tenant\n2 independent and interconnected office buildings in\nBELGIUM\nOffices Core 99m-102m Excellent Refurbished in 2014 Occupancy 92% Leopold / Location : A/ Accessibility : very good, in front\nBrussels\nof the property / Construction : 1992 / WALT : 7 years/\nLocated in the heart of CBD - next to the metro stop\nNETHERLANDS Beurs - Single good tenant - comprehensive\nOffices Core 85m-90m Excellent Recent building Fully let\nRotterdam refurbishment undergoing - handover scheduled for June\n2020 - 207 parking spaces\nLocated in the port of Rotterdam - connected with the\nFully let\nNETHERLANDS A15 highway - Sale and lease back with 10 years triple-\nLogistics Core 50m - 55m Good New building\nRotterdam net lease agreement - 43 loading docks - Expected\nSingle tenant\ncompletion in Q1 2020\n17 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 15,
|
||||
"text": "Indicative pipeline of Investments — June 2019\n; Technical\nCountry / city Sector Risk Deal size Location De Tenancy Comments\nspecificities\nThe building is a recently constructed office property in\nthe European District of the Brussels CBD. Located close\nFully letto five\ntothe Rue de la Loi, the main axis of the district, the\nBELGIUM Offices Core Good New building tenants (WALT of\n34-40m building benefits from close proximtio ttyhe main EU\nBrussels almost 10 Y)\ninstitutions, excellent infrastructure and many amenities.\nSpecifications fully meet today's market requirements,\nincluding a BREEAM Excellent rating.\nCompletely Fully let Office located in the West Berlin — Good Tenant -\nGERMANY Offices Core 44m-46m Se refurbished\nSingle tenant Delivery Q4 2020 - Lease term 20 years -\nBerlin\nA recent office building constructed in 2017, with 7 700\nsqm, 59 parking spaces and amenities (conference\nAlmost fully let\ncentre, fitness club, cafe, restaurant...). The property has\nPOLAND (WAULT 10 years)\nOffices Core 50-75m Excellent Recent building a Leed Platinium certification. The building is very well\nWarsaw\nlocated in the City Center of Warsaw and benefits from\nMulti tenant\nan excellent access to public transport. The expected\nyield is around 4,50%.\nNew 17 700 sqm development completed at the end of\nFully rented 2019, located seaside, in the South West city center of\nFINLAND\nOffices Core Good New building Helsinki. The property is single let to an Agency of the\nHelsinki 100-150m\nSingle tenant European Union on a 10 year lease agreement. The\nexpected yield is around 4,50%.\nAn outstanding quality iconic and sensitively developed\nFully rented\nCZECH Republic multifunctional building consisted of a restored baroque-\nOffices Core CBD New building\nPrague 90-100m renaissance palace from 1734, juxtaposed with a 2018\nMulti tenants\nconstructed eight storey premium office building\nFully rented\nFRANCE Property newly built in an established office submarket\nOffices Core Good New building\nSaint Denis 150 -170m close to public transports.\nMulti tenants\n18 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 16,
|
||||
"text": "Expertise\nOur strategy in Europe\nInvestment Process\nESG framework\nTeams\n19 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 17,
|
||||
"text": "Strategy to access & select prime assets in Europe\nGED benefits from long history, strong local partnerships, global and CRE economic research\nAcquisitions 2016-2018: over €11bn\nDeep deal flow in Europe = France\n= Paris exceptional deals\n\\\n= Germany\nGERM sources assets across Europe. All segments of\n= Netherlands\nreal estate assets are covered, with a focus on offices. = Italy\nThanks to the importance of its inflows, @jiiicarries = Austria\nout the largest transactions in the European market, Rane Rep\nwith €4bn of acquisitions out of France over the past 3 LUXENDBUG\nyears. en\n= UK\n= Finland\n2015 2016 2017 2018\nPipeline 620 assets analysed 520 assets analysed 577 assets analysed 813 assets analysed\nP €41.6 Bn €51.6 Bn €63.8 Bn €66.3Bn\nee 126 assets 74 assets 86 assets 79 assets\nCommittee €5.7 Bn €11.1 Bn €17.3 Bn €8.8 Bn\nAegucit 52 assets 55 assets 30 assets 17 assets\ncq7u isitions €2.6 Bn €4.3 Bn €6.0 Bn* €1.1 Bn\n20 GE: :: Estate Prime Europe EEE"
|
||||
},
|
||||
{
|
||||
"page": 18,
|
||||
"text": "An open-architecture organisation\nAllowing for flexibility and agility\n- Semi-open architecture based on a strong\nintegrated platform in France, Luxembourg,\nItaly;\n- Longstanding partnerships in major\nEuropean countries, giving us the required local\nexpertise and the important flexibility to choose\nwhere to invest in Europe;\n- A strict and documented methodology when\nselecting our partners, in terms of compliance\nwith our ESG policy (target 2021).\nCountry Partners (non-exclusive)\nGermany Etoile Properties\nAerium\nIC Property Investment &\nManagement\nBenelux Etoile Properties\nHannover Leasing\nUK Knight Frank\nScandinavia Newsec\nAustria EHL\nIberia Etoile Properties\nFY Estate, December 2018\nEEE\n21 GD : :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 19,
|
||||
"text": "Investment process\nSR: Estate Prime Europe\nSourcing - Continuous market watch: discussions with our local partners, brokers, sellers\n- Dealflow in open architecture\n- First screening, analysis, identification of due diligence issues\n- Target portfolio guidelines\n- Thorough financial and ESG analysis of the asset, external valuation and\nasset visit\n- Technical, legal, tax, notarial due diligence\n- Negotiation of financing in accordance with due diligences’\nconclusions (tender, term sheet, loan documentation)\n- Acquisition structuring to minimize risk and tax, closing\ndocumentation\n- Strategy & business plan\n- Coordination with property and asset\nmanager\n22 GEDea! Estate Prime Europe ZEEEEE"
|
||||
},
|
||||
{
|
||||
"page": 20,
|
||||
"text": "GEBhas a long experience of core/core+ investments\nin Europe\nGERD(a balanced pan-European open ended retail fund — under the form of a French collective undertaking for Real\nEstate investments “OPCI”) is the flagship oQf in France and combines RE and listed assets (respective targets of 60%\nand 40%) with max. 40% leverage. The RE portfolio of the fund is a good illustration Of expertise in European\ncore/core+ investments.\nOPCIMMO RE EN ee,\nportfolio ) ZZ\nTotal Return +4,6% +6,6% +7,5% +8,8% +7,2% +72% +6,9% ae ieer\nne irene 69 N Pologne\nRE AuM (€m) 99 297 472 1,636 3,214 4,846 4,920 a oe 25. a a\nIpStuRR:3 Ukraine\nNumber of 6 13 17 32 52 71 62 =RE-=)Seti\nassets Be — Be\nEr \\ ie ‘ulgarie\nRE Leverage 0% 234% 296% 307% 358% 378% 347% Th a Ra Re\nTurquie\nsource ‘GD :::: as of December 2018\nPast performance is not a guarantee of future results\nSESE\n23 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 21,
|
||||
"text": "Our ESG approach: a defined framework to reach the best\nstandards in terms of low carbon commitment\n| knaronmemal and ch! aertormaren\nGED will integrate additional investment criteria in order to be a green\nfund.\nAs such, following preselection of the assets, the fund managers will Laci\nexclude assets ranked below D, and build a portfolio with a global\nranking above or equal to C. rst\nOn top of this ranking, we will perform a specific analysis on each asset ne lese,\nin order to be fully aware of its impact in environmental terms:\n- The carbon footprint ofthe assets\n- The 2 degrees trajectory of the assets ‚=\n- The exposure ofthe assets to climate risks 3000 5 G\n} #\nIn line with the Paris Agreement — COP 21* and the European directive** objectives for foreign\nassets, we will assess for each asset the greenhouse gas emissions reductions to be\nachieved and implement an energy consumption reduction trajectory, delivering to our\nclients a genuine low carbon approach backed by concrete analyses and reporting\n“the Paris Agreement:\n- limiting the average increase in the planet’s temperature to less than 2°C compared to pre-industrial levels\n- reinforcing capacity for adaptation to the harmful effects of climate change and promoting resilience to these changes\n**the European Directive :\n- The European Council has adopted an indicative objective to reducing energy consumption by 27% by 2030\nSESE\n24 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 22,
|
||||
"text": "A robust and balanced setup for efficient teamwork\nReal Estate\n(125 people)\nInstitutional Solutions\nReal Estate team\nInvestment teams\n(Research, Acquisitions\n& Sales, Asset\n| Advisory\nManagement)\nOperations (Fund\nControlling, Liability\nManagement... )\nMngt co.\nLuxembourg\n(76 people)\n| Ptf and Risk\n: Mngt Strong and long dated\nexperience as an AIFM\nGeneral Partner GP Sarl 4 people dedicated to\nLux real estate: Conducting\nOfficer, Portfolio\nManagement, Fund\nControlling and\ndedicated\nRisk&Compliance\nOfficer\n25 a. :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 23,
|
||||
"text": "Investment Case & Views\n28 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 24,
|
||||
"text": "Market\nViews\nEurope is a liquid, deep and attractive real estate market\n; Spread between office prime yield and 10-\nInvestment volumes in European real estate RR a. |\nyear Govies (in basis points, since 2000)\n300 Md € 4 r 6% 600 pb\n250 Md € 4 | 5% : 4\nS\n0\nU\n0\nR\np\nph\nb FE\nma€ x\n200 Md € + L 4% 300 pb all.\n200 pb a eo a ce i\n150 Md€ 4 | 3% 100 pb 4 TT =\n= ao . j | o am 0 pb min\n-100 pb —\n50 Md € + L 1% -200 pb\nO Md € + 0% -300 pb \" & = m P\n19 11 12 13 14 18 16 17 18 19 FS F KC Kw K & KS N!\nGa Q3\nPrcS\nme 2\nma CO! — Q2 2019 — Average 200— 0Q2 2019 —- (04 2018\nwm Average S1 2010 - 2019\n—EU 15 weighted prime rate (right scale) “End of period\n- Europe is a key destination for capital markets - Real Estate investors follow their acquisitions at historically\nlow rates, this behavior is led by office rents increases\n- European investment markets have been very active over the recent anticipations (and to some extent for the logistics), at low\nyears. Offices are the main asset class, with a little less than 1 euro financing rates, and a gap with 10 years rates significantly\nover 2 euros invested. higher than the long period average.\n- Local European actors and even often local national investors have - The spread between government bonds and prime yields is\ndominated the RE investment markets in Europe, but international still currently significantly high on many markets in a lasting\ninvestors are gaining market shares (mainly from Asia, Americas and low rates environment\nthe Middle East) .\n29 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 25,
|
||||
"text": "Market\nViews\nOffices are by far the main real estate asset class in Europe\nview of cities’ office prime headline\nOffice take-up and vacancy rate\nrents — Q2 2019\n14Mm? - - 12%\nAmsterdam Prague\nBarcelone\n12Mm? + + 10% Hambourg\nBruxelles\nFrancfort\nMilan\n10 Mm? 4 6.0 8%\nDublin\nLondres Paris\n8 Mm? 4 + 6% Varsovie\n6M mi» | 4% Berlin\nMunich\n4M mi - 1 2%\n2Mm? + + 0%\n0OMm?« -2% A d c e c c e li l n e e r ation of rents S de l c o l w i d ne o wn of rents A i c n c c e r l e e a r s a e t ion ofrents i S n l c o r w e d a o se wn of rents\n10 11 12 13 14 15 16 17 18 19\n(4\nma3\nRents variation intensity: weak moderate strong\nme)\nu() |\nu Average S1 2010-2019\n— Vacancy rate EU 15 {right scale)\newes Headline prime rent rental growth EU 15 (rught scale)\n- The office market has been particularly active at the 1° semester - Major EU markets should benefit from tenants demand\n2019, a performance to be highlighted in a context of economic\n- All markets have different rental cycles in terms speed: Pan\nslowdown and of high uncertainties: office commercialization have\nEuropean diversification will allow to anchor RE investment\nincreased over 1 year in Western Europe and are higher than the\nperformance\ndecade average.\nNB\n- Alot of companies continue to favour central zones for “talents\n- The positions are purely indicative and are not an investment recommendation or\nhunting” recruitment purposes., but they face a quality offer that is solicitation\n- City positions can move at different speeds and directions depending on various\nregularly lacking. This context of rarity, if it benefits “In white”\nparameters\nlaunches, it exerts upward pressure on facial rents.\nSource!\n30 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 26,
|
||||
"text": "A strategy for Europe today: stability & diversity\nCurrent Fund Target Allocation across Europe\nOur strategy for Continental Europe today: BE Conviction - Strong\n> targeting sustainable LT yield, value protection, with a ME Conviction - Medium\nhigh level of diversification and valuation potential FE Conviction - Low\nFrance Germany Benelux Other € Non € Target Themes\n/ Austria\nTargeting demand-driven markets featuring rent recovery or rent\nOffices 70% pressures and >200bps risk premiums over risk free rates, always\nin prime locations\nFocus on leases featuring fixed or floored rents with established\nHospitality\n= + + + = operators in markets where constrain of new offer exist. Risk\nHotels, others 10% premium must reflect any operational risk taken\nindustrial / Look at the opportunities in close to city centers distribution\n\\Warahöus + of. + = = 10% platforms while large modern logistic platforms might be out of\na e reach (high individual values).\nRetail (High Focus on prime high street retail or small central urban shopping\nstreet; retail + + + + - 10% centers in main secondary cities demonstrating positive data in\npark) terms of demographics and spending potential\nTier 1 (FR, UK, DE, BE, NL, LU, Nordics, SP, IT, CH) target 80%\nTier 2 target 20%\n31 GE : ::: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 27,
|
||||
"text": "Current views accross Europe\nThose markets are diverse and present perspectives and positions in Real BM Conviction - Strong\nEstate cycles which are highly dependent upon each local economic situations, BM Conviction - Medium\nperspectives and exceptional events affecting them:\n— Conviction - Low\na|\nGermany France Portugal\nPrime office assets in Prime markets are very Paris region is a deep and liquid market. Rents Lisbon is a small market but it experienced a\npricey unless rent reversion is real. Risk have some potential to improve. Considering rapid economic recovery in recent years and is\npremium remains attractive on a leveraged current low yield and fierce competition, office interesting for Core Offices, quality Retail asset\nbasis. Managteo core or build to core can make right outside CBD for Core + assets can be or Hotel walls with top operators. Limited\nsense as a LT investor in main cities. considered. Manage to core strategies could liquidity of this market means investment must\nResidential is also attractive make sense. be small\n||]\nBenelux Austria United Kingdom\nBrussels is an interesting market despite its high Office retail and hotel marketsto be looked at London office market attractiveness has\ndependency on EU commissions offices. as risk premium remains attractive and financing been hurt by the uncertainties introduced\nOpportunities are rare but worth looking at. offer as good conditions as in Germany. We since Brexit vote. Although presenting new\nDespite a recent decrease in yields, Amsterdam must remain cautious in this market where opportunities, the UK market does not\nand Luxemburg remain attractive for office and competing future supply can break present present today the best investment set.\nresidential equilibrium.\nIreland Finland Italy\nThe market is narrow but opportunities can Although rather small market, Finish office, retail Office market is over priced and leverage is not\nbe looked at in the Dublin office market, and hotel assets should be looked at. Asset size efficient. Each of Milan and Rome office\nwhich can benefit from Brexit. A particular should remain reasonable as this market lacks markets are narrow. competition is currently too\nfocus on the competitive future supply will be liquidity. Residential market can also be looked strong for prime assets. Focus could be made\nneeded. at although local investors present strong on retail in 2nd tier cities for best in class micro\ncompetition locations.\nPoland Spain Czech Rep\nPrime office market in Warsaw will be searched Madrid office and retail assets are interesting Office assets could be looked at on an\nfor opportunities as sellers (investors and while financing costs continue to decrease and opportunistic basis in Prague but this market is\ndevelopers) start to be reasonable in their economy slowly recovers, offering potential for now very competitive and can sometime be\nselling price. Investment should focus on Euro higher rents. Assets in prime locations (Madrid overpriced especially for Euro-denominated\ndenominated assets offering mainly Euro and Barcelona) should be favored as they have deals.\nrevenues. best potential for rent evolutions.\nSESE\n32 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 28,
|
||||
"text": "Appendices\nFocus on our ESG approach\nFund Information / Reportings\nRecent acquisitions in Europe\nBiographies\n33 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 29,
|
||||
"text": "Focus on the ESG approach\nA) The environmental and social mapping\nPerformance environnementale et sociale\nTo realize this mapping, we mainly use the\n71 / 100\nBREEAM-In-Use part 1 frame of\nG F E D C B A reference.\nee DE\n0 14 29 43 9 6 100\nThis internationally known frame of\nPollution 29% reference allows us to confirm the\n59% ~~. Santé et bien-étre\nrelevance of the realised analyses.\n52%\n100% OccupPatir =\nsol et écologie\nNous y dérogeons sur l’aspect énergie en\nDechets\n100% |\ns’interessant a l’annee de construction du\nFE ' Energie\nPerennite des \\ 65% bätiment et a la reglementation a laquelle\néquipements il etait soumis.\n78% Transport\nWe derogate from it on the energy aspect\n83%\nby focusing on the construction year of the\nbuilding and the rules and regulations to\nwhich it was subject.\n34 |"
|
||||
},
|
||||
{
|
||||
"page": 30,
|
||||
"text": "Focus on the ESG approach\nB) Energy-Carbon performances evolutions\nThis part is based on the consumption of the\nEvolution des performances Energie - Carbone\nasset. It allows to visualize the evolution of\nconsumption in relation to two objectives:\nEnergie Carbone\n(kWh,,/m?.an) | (kgCO,e/m?.an)\nPerformance de - Energetic objective: based on the\nréférence 286 17 reductions imposed by the tertiary decree in\n(2011)\nFrance and on recommended reductions by\nPerformance actuelle the European framework for energy and\n286 17\n(2011) climate for other European countries.\nObjectif 2030 171 14\n- Carbon objective: based on the necessary\nAvancement de reduction to ensure that the asset is on a\n100\nl'objectif (%) 0%\n[0 trajectory compatible with the Paris\nAgreement limiting the global warming to\n2 os\niy Les performances analysees prennent en compte :\n- les usages de l'immeuble (parties communes et/ou privatives)\n- le mix énergétique de chaque pays d'implantation (pour la The translation of the energetic performances\nconversion Energie-carbone)\nin carbon performances takes into account the\nLa performance carbone est issue des consommations\nenergetic mix of the asset's country.\nénergétiques uniquement (scope 1 et/ou 2).\n35 |"
|
||||
},
|
||||
{
|
||||
"page": 31,
|
||||
"text": "Focus on the ESG approach\nC) Exposure to climate risks\nRisques physiques lies au changement climatique\nLes risques physiques lies au changement climatique se traduisent par des evenements chroniques (élévation du niveau de la mer et de la\ntempérature) et exceptionnels (canicules, inondations, tempétes) pouvant endommager le batiment ou ses équipements.\nkz ow ee BER\nLe batiment est situé dans un milieu\nurbain dense, avec un phénoméne d’ilot\ni de chaleur urbain lors de fortes chaleurs\npouvant entrainer des appels de\npuissance supplémentaires en froid.\nHausse du Inondations dues Hausse de la Canicules Tempétes\nniveau de la mer aux pluies température moyenne\nThis part allows an evaluation of the exposure of the asset to 5 risks linked to climate changes. 3\ncriteria are considered to build this grade:\n- The geolocation of the asset and the resulting predictive scenarios of the climate change ;\n- The devices and characteristics of the asset allowing it to resist to these risks ;\n- The immediate environment of the asset that may include aggravating factors.\nre\n36 |"
|
||||
},
|
||||
{
|
||||
"page": 32,
|
||||
"text": "ESG\napproach\nFocus on our ESG approach: mobilization within our sector\nGERD Real Estate is one of the founding members of the Observatory of\nSustainable Real Estate, an independent and transparent forum for exchanges,\npromoting the sustainable development and innovation of French real estate. ®) B\npESOCIATing\nBATIMENT\nGHEE Real Estate is a member of the BBCA low carbon building association since\nBBCA\n2016.\nBAS CARBONE\nC,\nMEMBRE 2019\nGERD Real Estate participates in the working group on the creation of A SPIM ASSOCIATION FRANGAISI\nICIETES\nan SRI label applied to real estate. ACEMENT IMMOBILIET\nps\n33 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 33,
|
||||
"text": "Fund\nReporting\nFund Information Reporting\nFinancials : quarterly (D+45) Real Estate : semi-annually (D+90)\n- NAV calculation, consolidated accounts - Appraisal reports, global market review, asset\n- InREV adjustments, distributed dividends management report\nstatements (semi-annual), ratios =\nss | Be | == |e | ee\nGlobal synthesis : annually (D+90) Annual Report (D+120)\n- Annual accounts, market, financial and real estate - Annual audited accounts\nanalyses, updated business plan - Audit report\n— Be - - un\n-/— | naa\n3 =\n= 1\nWw oo Real Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 34,
|
||||
"text": "Recent deals in Europe\n(1/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nI [rem 00 Bo\nInvestment by unds\nINK MGallery Hotel Area: 148 rooms\nAmsterdam ! ee\nCore Tenant: Accor Hospitality Nedeland\nClosed in 2018 WAULT (years): 14\nAsset Value: €64,6m\nNet Initial Yield: 4.02%\nUnlevered CoC: 4.14%\nUnlevered IRR 10Y: 4.40%\nEl Portico Area: Office 20,814 sqm Investment by unds\nMadrid Parking 401 units\nCore Tenants: Multi tenants\nClosed in 2018 WAULB (years): 2,28\nPrice: €117,4m\nNet Initial Yield: 4.31%\nUnlevered CoC: 3.31%\nUnlevered IRR 10Y: 4.26%\n39 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 35,
|
||||
"text": "Examples of recent deals in Europe\n(2/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nroJmoe\nEnjoy Area: Office 16,970 sqm Forward sale deal,\nParis development to be\nCore Parking 64 units completed by end-2018.\nClosed in 2018 Tenant: AXA Services\nWAULT (years): 9 Co-investment between\nAsset Value: €258m net Fund and a French\nNet Initial Yield: 3.40% institutional investor\nUnlevered CoC: 3.30%\nUnlevered IRR 10Y: 3.30%\nArea: Office 28,564sqm Investment b¥@iiiFunds\nBBW Residential 2,494 sqm\nFranfurt Commercial 1,028 sqm\nCore Parking 347 units\nClosed in 2018 Mains tenants: KfW, Dwp Bank, Nomura\nWAULT (years): 10\nAsset Value: €141,2m\nNet Initial Yield: 4.25%\nUnlevered IRR 10Y: 3.60%\n40 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 36,
|
||||
"text": "Recent deals in Europe\n(3/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nDM [rom Terms\nGrand Central Area: Office 43,674 m? Forward sale deal,\nFrankfurt Storage 1,636 m? development to be\nCore Parking 783 units completed by end-2020.\nClosed in 2017 Tenant: Deutsche Bahn Netz AG (100%)\n-subsidiary of DB AG*\nWAULT (years): 20 GERD funds bought 100% of\nAsset Value: €324m net the deal and seek to share\nNet Initial Yield: 3.45% 50% of this deal with co-\nUn-levered Cash-on-cash: 3.48% investor(s).\nInvestor un-levered IRR 10Y: 3.22%\nRocket Tower Area: Office 26,192 m? Co-investment between\nBerlin Retail 1,765 m? GERuDnd and a Finnish\nCore Parking 411 units institutional investor\nClosed in 2017 Tenants: Multi (occupancy 96%)\nWALB (years): 13:5\nPrice: €149m net\nNet Initial Yield: 40%\nLTV: 45%\nCash-on-cash: 9.47%\nInvestor IRR 10Y: 7.58%\nmm \"DUB AG: responsible for railways maintenance\n41 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 37,
|
||||
"text": "Recent deals in Europe\n(4/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nECT [>777\nCoeur Défense Area: Office 182,765 m? Co-investment between\nParis, La Défense Main tenants: HSBC, RTE, Allianz and EDF EN and French\nCore WALB (years): 7 institutional investors\nClosed in 2017 Price: € 1,720m\nNet Initial Yield: 4,78%\nLTV: 52%\nCash-on-cash: 5.59%\nInvestor IRR 10Y: 7.46%\nTour Hekla Area: Office 79,876 m? Speculative development\nParis, La Défense Main tenant: Vacant Co-investment between\nCore WALB (years): and a French\nClosed in 2017 Price: € 582m institutional investor.\nNet Initial Yield: 6.7%\nLTV: 41%\nCash-on-cash: 5.8%\nInvestor IRR 10Y: 6.03%\n42 GE: :: Estate Prime Europe"
|
||||
},
|
||||
{
|
||||
"page": 38,
|
||||
"text": "Recent deals in Europe\n(5/5) Our sourcing capabilities make us a specialist of European assets’ origination and asset management\nroens\nThe Atrium Area: Office 59,044 m? Co-investment wit»\nAmsterdam south-axis Parking 525 units investors\nCore Tenants: Multi (occupancy 68%)\nClosed in 2017 WALB (years): 8\nPrice: €920m net\nNet Initial Yield: 3.83%\nLTV: 60%\nCash-on-cash: 6-7%\nInvestor IRR 10Y: 7.57%\nThe Cloud Area: Office 23,807 m? Co-investment between\nAmsterdam Parking 195 units GERD0 ans GD\nCore Tenants: Multi (occupancy 98.4%) institutional investor\nClosed in 2017 WALB (years): 9.7\nPrice: €159m net\nNet Initial Yield: 4.24%\nLy: 40%\nCash-on-cash: 4.78%\nInvestor IRR 10Y: 5.56%\n43 GE: :: Estate Prime Europe"
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,74 @@
|
|||
annotated-types==0.7.0
|
||||
blinker==1.9.0
|
||||
blis==0.7.11
|
||||
catalogue==2.0.10
|
||||
certifi==2025.4.26
|
||||
cffi==1.17.1
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
cloudpathlib==0.16.0
|
||||
confection==0.1.5
|
||||
cryptography==44.0.2
|
||||
cymem==2.0.11
|
||||
Deprecated==1.2.18
|
||||
deprecation==2.1.0
|
||||
filelock==3.18.0
|
||||
Flask==3.1.0
|
||||
fsspec==2025.3.2
|
||||
huggingface-hub==0.30.2
|
||||
idna==3.10
|
||||
img2pdf==0.6.1
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
langcodes==3.5.0
|
||||
language_data==1.3.0
|
||||
lxml==5.4.0
|
||||
marisa-trie==1.2.1
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.2
|
||||
mdurl==0.1.2
|
||||
mpmath==1.3.0
|
||||
murmurhash==1.0.12
|
||||
networkx==3.4.2
|
||||
numpy==1.26.4
|
||||
ocrmypdf==16.10.1
|
||||
packaging==25.0
|
||||
pdfminer.six==20250327
|
||||
pdfplumber==0.11.6
|
||||
pi_heif==0.22.0
|
||||
pikepdf==9.7.0
|
||||
pillow==11.2.1
|
||||
pluggy==1.5.0
|
||||
preshed==3.0.9
|
||||
pycparser==2.22
|
||||
pydantic==2.11.3
|
||||
pydantic_core==2.33.1
|
||||
Pygments==2.19.1
|
||||
PyMuPDF==1.25.5
|
||||
pypdfium2==4.30.1
|
||||
PyYAML==6.0.2
|
||||
regex==2024.11.6
|
||||
requests==2.32.3
|
||||
rich==14.0.0
|
||||
safetensors==0.5.3
|
||||
smart-open==6.4.0
|
||||
spacy==3.7.2
|
||||
spacy-alignments==0.9.1
|
||||
spacy-legacy==3.0.12
|
||||
spacy-loggers==1.0.5
|
||||
spacy-transformers==1.3.3
|
||||
srsly==2.5.1
|
||||
sympy==1.14.0
|
||||
thinc==8.2.5
|
||||
tokenizers==0.15.2
|
||||
torch==2.1.0
|
||||
tqdm==4.67.1
|
||||
transformers==4.35.2
|
||||
typer==0.9.4
|
||||
typing-inspection==0.4.0
|
||||
typing_extensions==4.13.2
|
||||
urllib3==2.4.0
|
||||
wasabi==1.1.3
|
||||
weasel==0.3.4
|
||||
Werkzeug==3.1.3
|
||||
wrapt==1.17.2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue