Compare commits
10 Commits
a52c4c808e
...
3ae8295c26
| Author | SHA1 | Date |
|---|---|---|
|
|
3ae8295c26 | |
|
|
68907be99f | |
|
|
28534d4774 | |
|
|
ac08c67332 | |
|
|
019e10d5b8 | |
|
|
6fef07ac86 | |
|
|
0a64411a5f | |
|
|
23f047df4e | |
|
|
dc9d693768 | |
|
|
360d7f4906 |
|
|
@ -1,7 +1,6 @@
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import logging
|
import logging
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
@ -17,6 +16,18 @@ TIMEOUT = 180
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_dynamic_labels():
|
||||||
|
url = f"{COORDINATOR_URL}/api/kpi_setting/"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
kpi_list = response.json()
|
||||||
|
labels = [kpi["name"].upper() for kpi in kpi_list if kpi.get("active", False)]
|
||||||
|
return labels
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Konnte dynamische Labels nicht laden: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def extract_with_exxeta(pages_json, pitchbook_id):
|
def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
|
@ -30,9 +41,7 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
if i % 8 == 0:
|
if i % 8 == 0:
|
||||||
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35 + 60/len(pages_json)*i})
|
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 35 + 60/len(pages_json)*i})
|
||||||
|
|
||||||
|
|
||||||
page_num = page_data.get("page")
|
page_num = page_data.get("page")
|
||||||
page_data.get("page")
|
|
||||||
text = page_data.get("text", "")
|
text = page_data.get("text", "")
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
|
|
@ -51,9 +60,9 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
"- Gib die Antwort als **JSON-Array** im folgenden Format zurück:\n\n"
|
"- Gib die Antwort als **JSON-Array** im folgenden Format zurück:\n\n"
|
||||||
"[\n"
|
"[\n"
|
||||||
" {\n"
|
" {\n"
|
||||||
" \"label\": \"FONDSNAME\",\n"
|
' "label": "FONDSNAME",\n'
|
||||||
" \"entity\": \"...\",\n"
|
' "entity": "...",\n'
|
||||||
f" \"page\": {page_num},\n"
|
f' "page": {page_num},\n'
|
||||||
" },\n"
|
" },\n"
|
||||||
" ...\n"
|
" ...\n"
|
||||||
"]\n\n"
|
"]\n\n"
|
||||||
|
|
@ -61,45 +70,29 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
f"TEXT:\n{text}"
|
f"TEXT:\n{text}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
labels = get_dynamic_labels()
|
||||||
|
prompt_kennzahlen = "".join([f"- {label}\n" for label in labels])
|
||||||
prompt = (
|
prompt = (
|
||||||
"Bitte extrahiere relevante Fondskennzahlen aus dem folgenden Pitchbook-Text. "
|
"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"
|
"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"
|
"ZU EXTRAHIERENDE KENNZAHLEN (immer exakt wie unten angegeben):\n"
|
||||||
"- FONDSNAME\n"
|
f"{prompt_kennzahlen}\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"
|
"WICHTIG:\n"
|
||||||
"- Gib **nur eine Entität pro Kennzahl** an - keine Listen oder Interpretationen.\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"
|
'- 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"
|
"- **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"
|
"- Extrahiere **nur wörtlich vorkommende Inhalte** (keine Berechnungen, keine Zusammenfassungen).\n"
|
||||||
"- Jeder gefundene Wert muss einem der obigen Label **eindeutig zuordenbar** sein.\n\n"
|
"- Jeder gefundene Wert muss einem der obigen Label **eindeutig zuordenbar** sein.\n\n"
|
||||||
|
|
||||||
"FORMAT:\n"
|
"FORMAT:\n"
|
||||||
"Antworte als **reines JSON-Array** mit folgendem Format:\n"
|
"Antworte als **reines JSON-Array** mit folgendem Format:\n"
|
||||||
"[\n"
|
"[\n"
|
||||||
" {\n"
|
" {\n"
|
||||||
" \"label\": \"Kennzahlname (exakt wie oben)\",\n"
|
' "label": "Kennzahlname (exakt wie oben)",\n'
|
||||||
" \"entity\": \"Wert aus dem Text (exakt im Original)\",\n"
|
' "entity": "Wert aus dem Text (exakt im Original)",\n'
|
||||||
f" \"page\": {page_num},\n"
|
f' "page": {page_num},\n'
|
||||||
" },\n"
|
" },\n"
|
||||||
" ...\n"
|
" ...\n"
|
||||||
"]\n\n"
|
"]\n\n"
|
||||||
|
|
||||||
f"Falls keine Kennzahl enthalten ist, gib ein leeres Array [] zurück.\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"Nur JSON-Antwort - keine Kommentare, keine Erklärungen, kein Text außerhalb des JSON.\n\n"
|
||||||
f"TEXT:\n{text}"
|
f"TEXT:\n{text}"
|
||||||
|
|
@ -125,10 +118,7 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, headers=headers, json=payload, timeout=TIMEOUT)
|
response = requests.post(url, headers=headers, json=payload, timeout=TIMEOUT)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
content = response.json()["choices"][0]["message"]["content"].strip()
|
||||||
content = response.json()["choices"][0]["message"]["content"]
|
|
||||||
content = content.strip()
|
|
||||||
|
|
||||||
if content.startswith("```json"):
|
if content.startswith("```json"):
|
||||||
content = content.split("```json")[1]
|
content = content.split("```json")[1]
|
||||||
if content.endswith("```"):
|
if content.endswith("```"):
|
||||||
|
|
@ -143,14 +133,16 @@ def extract_with_exxeta(pages_json, pitchbook_id):
|
||||||
if isinstance(page_results, list):
|
if isinstance(page_results, list):
|
||||||
results.extend(page_results)
|
results.extend(page_results)
|
||||||
break
|
break
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
if attempt == MAX_RETRIES:
|
if attempt == MAX_RETRIES:
|
||||||
results.extend([])
|
results.extend([])
|
||||||
except Exception as e:
|
except Exception:
|
||||||
if attempt == MAX_RETRIES:
|
if attempt == MAX_RETRIES:
|
||||||
results.extend([])
|
results.extend([])
|
||||||
|
|
||||||
|
|
||||||
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95})
|
requests.post(COORDINATOR_URL + "/api/progress", json={"id": pitchbook_id, "progress": 95})
|
||||||
return json.dumps(results, indent=2, ensure_ascii=False)
|
return json.dumps(results, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("📡 Test-Aufruf get_dynamic_labels:")
|
||||||
|
print(get_dynamic_labels())
|
||||||
|
|
@ -330,6 +330,9 @@ export function ConfigTable({ from }: ConfigTableProps) {
|
||||||
>
|
>
|
||||||
<span title={`Click to view details (ID: ${kennzahl.id})`}>
|
<span title={`Click to view details (ID: ${kennzahl.id})`}>
|
||||||
{kennzahl.name}
|
{kennzahl.name}
|
||||||
|
{kennzahl.mandatory && (
|
||||||
|
<span> *</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "12px" }}>
|
<td style={{ padding: "12px" }}>
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ export function PitchBooksTable() {
|
||||||
{status === "completed" ? (
|
{status === "completed" ? (
|
||||||
<Chip
|
<Chip
|
||||||
icon={<CheckCircleIcon />}
|
icon={<CheckCircleIcon />}
|
||||||
label="Abgeschlossen"
|
label="Extraktion Abgeschlossen"
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#e8f5e9",
|
backgroundColor: "#e8f5e9",
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export default function UploadPage() {
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#383838",
|
color: "#383838",
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
marginTop: 6,
|
marginTop: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Pitchbook Extractor
|
Pitchbook Extractor
|
||||||
|
|
@ -207,7 +207,7 @@ export default function UploadPage() {
|
||||||
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
|
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
|
||||||
onClick={() => navigate({ to: "/pitchbooks" })}
|
onClick={() => navigate({ to: "/pitchbooks" })}
|
||||||
>
|
>
|
||||||
Alle Pitch Books anzeigen
|
Alle Pitchbooks anzeigen
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
|
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
|
||||||
import { Box, Button, Paper, Typography, Snackbar, Alert, IconButton } from "@mui/material";
|
import { Box, Button, Paper, Typography, Snackbar, Alert, IconButton, Tooltip } from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
@ -50,6 +50,8 @@ function ExtractedResultsPage() {
|
||||||
const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook));
|
const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook));
|
||||||
const { data: settings } = useSuspenseQuery(settingsQueryOptions());
|
const { data: settings } = useSuspenseQuery(settingsQueryOptions());
|
||||||
|
|
||||||
|
const fundName = kpi["FONDSNAME"]?.[0]?.entity;
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
let hasRedBorders = false;
|
let hasRedBorders = false;
|
||||||
let hasYellowBorders = false;
|
let hasYellowBorders = false;
|
||||||
|
|
@ -158,7 +160,9 @@ function ExtractedResultsPage() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
<strong>Extrahierte Kennzahlen</strong>
|
<strong>
|
||||||
|
{fundName ? `Kennzahlen extrahiert aus: ${fundName}` : "Extrahierte Kennzahlen"}
|
||||||
|
</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -235,17 +239,29 @@ function ExtractedResultsPage() {
|
||||||
gap={2}
|
gap={2}
|
||||||
sx={{ flexShrink: 0 }}
|
sx={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<Button variant="contained" sx={{ backgroundColor: "#383838" }}
|
<Tooltip
|
||||||
onClick={handleCopyToClipboard}>
|
title={
|
||||||
<ContentPasteIcon sx={{ fontSize: 18, mr: 1 }} />
|
<>
|
||||||
{copied ? "Kopiert!" : "Kennzahlenzeile kopieren"}
|
<b>Kennzahlen kopieren</b>
|
||||||
</Button>
|
<br />
|
||||||
|
Kopiert alle aktiven Kennzahlen als Excel-Zeile in die Zwischenablage. Kann direkt in Excel eingefügt werden.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Button variant="contained" sx={{ backgroundColor: "#383838" }}
|
||||||
|
onClick={handleCopyToClipboard}>
|
||||||
|
<ContentPasteIcon sx={{ fontSize: 18, mr: 1 }} />
|
||||||
|
{copied ? "Kopiert!" : "Kennzahlenzeile kopieren"}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ backgroundColor: "#383838" }}
|
sx={{ backgroundColor: "#383838" }}
|
||||||
onClick={() => navigate({ to: "/" })}
|
onClick={() => navigate({ to: "/" })}
|
||||||
>
|
>
|
||||||
Neu hochladen
|
Neues Pitchbook hochladen
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
export const formatDate = (dateString: string): string => {
|
export const formatDate = (dateString: string): string => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours() + 2).padStart(2, "0");
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
|
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
|
|
||||||
return `${hours}:${minutes} ${day}.${month}.${year}`;
|
return `${hours}:${minutes} ${day}.${month}.${year}`;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue