feat([2fd88f42]): initial setup of heatmap tool

This commit is contained in:
2026-02-04 11:30:47 +00:00
parent 9983df54aa
commit 3a85514820
24 changed files with 4433 additions and 920 deletions

View File

@@ -1 +1 @@
{"task_id": "2f688f42-8544-8109-8ca1-d66deb49571b", "token": "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", "session_start_time": "2026-02-04T10:26:40.359251"}
{"task_id": "2fd88f42-8544-80f2-b60c-e23321da9620", "token": "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", "session_start_time": "2026-02-04T11:03:55.308316"}

16
heatmap-tool/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Heatmap Tool
This directory contains a standalone web application for visualizing data from XLSX files as a heatmap on a map of Germany.
## Description
The application allows users to upload an XLSX file containing German postal codes (PLZ). It then aggregates the data by postal code and displays it as a heatmap, where the color and size of the markers represent the density of records. The tool automatically detects other columns in the file and creates dynamic filters, allowing users to slice the data and visualize different segments.
## Getting Started
To run the application, navigate to the `heatmap-tool` directory and follow the instructions in its `README.md` file.
```bash
cd heatmap-tool
```
Then follow the instructions in `heatmap-tool/README.md`.

View File

@@ -0,0 +1,20 @@
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the requirements file into the container at /app
COPY requirements.txt .
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application's code from the host to the container at /app
COPY . .
# Expose port 8000 to the outside world
EXPOSE 8000
# Command to run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,123 @@
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import pandas as pd
import io
from pydantic import BaseModel
from typing import Dict, List
app = FastAPI()
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
# --- In-memory Storage ---
df_storage = None
plz_column_name = None
# --- Dummy Geocoding Data (IMPORTANT: TO BE REPLACED) ---
# This is a tiny subset of German postal codes for demonstration purposes only.
# A real implementation MUST load a comprehensive PLZ dataset from a file
# (e.g., a CSV or GeoJSON file) for the application to be useful.
PLZ_COORDINATES = {
"10115": {"lat": 52.53, "lon": 13.38}, # Berlin
"20095": {"lat": 53.55, "lon": 9.99}, # Hamburg
"80331": {"lat": 48.13, "lon": 11.57}, # Munich
"50667": {"lat": 50.93, "lon": 6.95}, # Cologne
"60311": {"lat": 50.11, "lon": 8.68}, # Frankfurt
}
# --- Pydantic Models ---
class FilterRequest(BaseModel):
filters: Dict[str, List[str]]
# --- API Endpoints ---
@app.get("/")
def read_root():
return {"message": "Heatmap Tool Backend"}
@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
global df_storage, plz_column_name
if not file.filename.endswith('.xlsx'):
raise HTTPException(status_code=400, detail="Invalid file format. Please upload an .xlsx file.")
try:
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents), dtype=str) # Read all as string to be safe
df.fillna('N/A', inplace=True)
# --- PLZ Column Detection ---
temp_plz_col = None
for col in df.columns:
if 'plz' in col.lower():
temp_plz_col = col
break
if not temp_plz_col:
raise HTTPException(status_code=400, detail="No column with 'PLZ' found in the file.")
plz_column_name = temp_plz_col
# Normalize PLZ data
df[plz_column_name] = df[plz_column_name].str.strip().str.zfill(5)
# --- Dynamic Filter Detection ---
filters = {}
for col in df.columns:
if col != plz_column_name:
unique_values = df[col].unique().tolist()
filters[col] = sorted(unique_values)
df_storage = df
return {"filename": file.filename, "filters": filters, "plz_column": plz_column_name}
except Exception as e:
raise HTTPException(status_code=500, detail=f"An error occurred while processing the file: {e}")
@app.post("/api/heatmap")
async def get_heatmap_data(request: FilterRequest):
global df_storage, plz_column_name
if df_storage is None:
raise HTTPException(status_code=404, detail="No data available. Please upload a file first.")
try:
filtered_df = df_storage.copy()
# Apply filters from the request
for column, values in request.filters.items():
if values: # Only filter if there are values selected
filtered_df = filtered_df[filtered_df[column].isin(values)]
if filtered_df.empty:
return []
# Aggregate data by PLZ
plz_counts = filtered_df.groupby(plz_column_name).size().reset_index(name='count')
# --- Geocoding Step ---
# In a real app, this would be a merge/join with a proper geo dataset
heatmap_data = []
for _, row in plz_counts.iterrows():
plz = row[plz_column_name]
coords = PLZ_COORDINATES.get(plz)
if coords:
heatmap_data.append({
"plz": plz,
"lat": coords["lat"],
"lon": coords["lon"],
"count": row["count"]
})
return heatmap_data
except Exception as e:
raise HTTPException(status_code=500, detail=f"An error occurred while generating heatmap data: {e}")

View File

@@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
pandas
openpyxl
python-multipart

View File

@@ -0,0 +1,17 @@
version: '3.8'
services:
backend:
build: ./backend
ports:
- "8000:8000"
volumes:
- ./backend:/app
frontend:
build: ./frontend
ports:
- "5173:5173"
volumes:
- ./frontend:/app
- /app/frontend/node_modules # prevent host node_modules from overwriting container

24
heatmap-tool/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,20 @@
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /app
# Copy package.json and package-lock.json to the container
COPY package.json package-lock.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application's code
COPY . .
# Expose the port the app runs on
EXPOSE 5173
# Command to run the development server
CMD ["npm", "run", "dev"]

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Heatmap Tool</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3588
heatmap-tool/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.4",
"leaflet": "^1.9.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-leaflet": "^5.0.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/leaflet": "^1.9.21",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,86 @@
/* General App Styling */
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#root {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
text-align: center;
}
/* Layout Styling */
.App {
display: flex;
flex-direction: column;
height: 100vh;
text-align: left;
}
.App-header {
background-color: #282c34;
padding: 10px 20px;
color: white;
flex-shrink: 0;
}
.App-header h1 {
font-size: 1.5em;
margin: 0;
}
.App-main {
display: flex;
flex-grow: 1;
overflow: hidden; /* Prevent scrolling at the main level */
}
.control-panel {
width: 350px;
padding: 20px;
background-color: #333;
overflow-y: auto; /* Allow scrolling if filters are many */
flex-shrink: 0;
box-shadow: 2px 0 5px rgba(0,0,0,0.5);
z-index: 10;
}
.map-container {
flex-grow: 1;
background-color: #1e1e1e;
position: relative; /* Needed for loading/error overlays */
}
.map-container .error {
color: #ff6b6b;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
padding: 20px;
border-radius: 8px;
}
.map-container p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@@ -0,0 +1,82 @@
import { useState } from 'react';
import axios from 'axios';
import './App.css';
import FileUpload from './components/FileUpload';
import FilterPanel from './components/FilterPanel';
import MapDisplay from './components/MapDisplay';
// Define types for our state
export interface FilterOptions {
[key: string]: string[];
}
export interface HeatmapPoint {
plz: string;
lat: number;
lon: number;
count: number;
}
function App() {
const [filters, setFilters] = useState<FilterOptions>({});
const [heatmapData, setHeatmapData] = useState<HeatmapPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleUploadSuccess = (newFilters: FilterOptions) => {
setFilters(newFilters);
setHeatmapData([]); // Clear previous heatmap data
setError(null);
// Automatically fetch data with no filters on successful upload
handleFilterChange({});
};
const handleFilterChange = async (selectedFilters: FilterOptions) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.post('http://localhost:8000/api/heatmap', {
filters: selectedFilters,
});
setHeatmapData(response.data);
} catch (error: any) {
if (axios.isAxiosError(error) && error.response) {
setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`);
} else {
setError(`Failed to fetch heatmap data: ${error.message}`);
}
setHeatmapData([]); // Clear data on error
} finally {
setIsLoading(false);
}
};
return (
<div className="App">
<header className="App-header">
<h1>German PLZ Heatmap Tool</h1>
</header>
<main className="App-main">
<div className="control-panel">
<FileUpload
onUploadSuccess={handleUploadSuccess}
setIsLoading={setIsLoading}
setError={setError}
/>
<FilterPanel
filters={filters}
onFilterChange={handleFilterChange}
isLoading={isLoading}
/>
</div>
<div className="map-container">
{isLoading && <p>Loading map data...</p>}
{error && <p className="error">{error}</p>}
<MapDisplay heatmapData={heatmapData} />
</div>
</main>
</div>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,70 @@
// src/components/FileUpload.tsx
import React, { useState } from 'react';
import axios from 'axios';
interface FilterOptions {
[key: string]: string[];
}
interface FileUploadProps {
onUploadSuccess: (filters: FilterOptions) => void;
setIsLoading: (isLoading: boolean) => void;
setError: (error: string | null) => void;
}
const FileUpload: React.FC<FileUploadProps> = ({ onUploadSuccess, setIsLoading, setError }) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
setSelectedFile(event.target.files[0]);
}
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedFile) {
setError("Please select a file first.");
return;
}
if (!selectedFile.name.endsWith('.xlsx')) {
setError("Invalid file type. Please upload an .xlsx file.");
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
setIsLoading(true);
setError(null);
try {
const response = await axios.post('http://localhost:8000/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
onUploadSuccess(response.data.filters);
} catch (error: any) {
if (axios.isAxiosError(error) && error.response) {
setError(`Upload failed: ${error.response.data.detail || error.message}`);
} else {
setError(`Upload failed: ${error.message}`);
}
} finally {
setIsLoading(false);
}
};
return (
<div className="file-upload">
<h3>Upload XLSX File</h3>
<form onSubmit={handleSubmit}>
<input type="file" accept=".xlsx" onChange={handleFileChange} />
<button type="submit" disabled={!selectedFile}>Upload</button>
</form>
</div>
);
};
export default FileUpload;

View File

@@ -0,0 +1,75 @@
// src/components/FilterPanel.tsx
import React, { useState, useEffect } from 'react';
interface FilterOptions {
[key: string]: string[];
}
interface FilterPanelProps {
filters: FilterOptions;
onFilterChange: (selectedFilters: FilterOptions) => void;
isLoading: boolean;
}
const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLoading }) => {
const [selectedFilters, setSelectedFilters] = useState<FilterOptions>({});
// Reset selected filters when a new file is uploaded (filters prop changes)
useEffect(() => {
setSelectedFilters({});
}, [filters]);
const handleSelectionChange = (category: string, values: string[]) => {
setSelectedFilters(prev => ({
...prev,
[category]: values,
}));
};
const handleApplyClick = () => {
onFilterChange(selectedFilters);
}
const handleResetClick = () => {
setSelectedFilters({});
onFilterChange({});
}
if (Object.keys(filters).length === 0) {
return <div>Upload a file to see filter options.</div>;
}
return (
<div className="filter-panel">
<h3>Filters</h3>
{Object.entries(filters).map(([category, options]) => (
<div key={category} className="filter-group">
<label>{category}</label>
<select
multiple
value={selectedFilters[category] || []}
onChange={(e) =>
handleSelectionChange(category, Array.from(e.target.selectedOptions, option => option.value))
}
>
{options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
))}
<div className="filter-buttons">
<button onClick={handleApplyClick} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Apply Filters'}
</button>
<button onClick={handleResetClick} disabled={isLoading}>
Reset Filters
</button>
</div>
</div>
);
};
export default FilterPanel;

View File

@@ -0,0 +1,67 @@
// src/components/MapDisplay.tsx
import React from 'react';
import { MapContainer, TileLayer, CircleMarker, Tooltip } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import { HeatmapPoint } from '../App'; // Assuming type is exported from App.tsx
interface MapDisplayProps {
heatmapData: HeatmapPoint[];
}
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData }) => {
const germanyCenter: [number, number] = [51.1657, 10.4515];
// Simple scaling function for marker radius
const calculateRadius = (count: number) => {
return 5 + Math.log(count + 1) * 5;
};
// Simple color scaling function
const getColor = (count: number, maxCount: number) => {
const ratio = count / maxCount;
if (ratio > 0.8) return 'red';
if (ratio > 0.5) return 'orange';
if (ratio > 0.2) return 'yellow';
return 'green';
}
const maxCount = Math.max(...heatmapData.map(p => p.count), 1);
if (heatmapData.length === 0) {
return (
<div style={{ textAlign: 'center', paddingTop: '50px' }}>
<p>No data to display on the map.</p>
<p>Upload a file and apply filters to see the heatmap.</p>
</div>
);
}
return (
<MapContainer center={germanyCenter} zoom={6} style={{ height: '100%', width: '100%' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{heatmapData.map((point, idx) => (
<CircleMarker
key={idx}
center={[point.lat, point.lon]}
radius={calculateRadius(point.count)}
pathOptions={{
color: getColor(point.count, maxCount),
fillColor: getColor(point.count, maxCount),
fillOpacity: 0.7
}}
>
<Tooltip>
PLZ: {point.plz} <br />
Count: {point.count}
</Tooltip>
</CircleMarker>
))}
</MapContainer>
);
};
export default MapDisplay;

View File

@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

925
readme.md
View File

@@ -1,926 +1,13 @@
---
include_toc: true
gitea: none
---
# Projekt: Automatisierte Unternehmensbewertung & Lead-Generierung v2.2.1
# Heatmap Tool Project
## 1. Projektübersicht & Architektur
This project contains the implementation of the Heatmap Tool.
Dieses Projekt ist eine modulare "Lead Enrichment Factory", die darauf ausgelegt ist, Unternehmensdaten aus einem D365-CRM-System automatisiert anzureichern, zu analysieren und für Marketing- & Vertriebszwecke aufzubereiten.
## Directory
Die Architektur ist in mehrere, klar voneinander getrennte Funktionseinheiten gegliedert, die im Folgenden detailliert beschrieben werden.
## Architektur im Überblick
```text
I. DIE STEUERUNGS-EBENE (API & Ausführung)
└── app.py (Flask API Server, startet Jobs)
└── brancheneinstufung2.py (Der zentrale Orchestrator / Kommandozeile)
II. DIE KERN-PRODUKTIONSLINIE (Datenanreicherung)
└── data_processor.py (Der "Motor", führt die Arbeit aus)
├── google_sheet_handler.py (Spezialist für Google Sheets)
├── wikipedia_scraper.py (Spezialist für Wikipedia-Daten)
├── sync_manager.py (Spezialist für den D365-Abgleich)
└── helpers.py (Der "Werkzeugkasten" für alle)
III. DIE MARKETING-PRODUKTIONSLINIE (Content-Erstellung)
└── generate_marketing_text.py (Erstellt E-Mail-Texte)
└── INPUT: marketing_wissen_final.yaml (Die Wissensbasis)
IV. DIE WISSENSBASIS-FABRIK (ETL-Pipelines zur Erstellung der Wissensbasis)
├── build_knowledge_base.py (Baut die Marketing-KB aus der config.py)
├── expand_knowledge_base.py (Erweitert die Marketing-KB)
├── extract_insights.py (Baut die Marketing-KB aus Word-Dokumenten)
└── generate_knowledge_base.py (Erstellt einen Entwurf für die Marketing-KB)
V. DAS KLASSIFIZIERUNGS-SYSTEM (Job-Titel-Analyse)
├── contact_grouping.py (Klassifiziert Job-Titel)
└── knowledge_base_builder.py (Baut die Wissensbasis FÜR die Klassifizierung)
VI. DAS STANDALONE-WERKZEUG
└── company_deduplicator.py (Eigenständiger Duplikats-Check für externe und interne Listen)
VII. DIE STRATEGIE-SCHMIEDE (GTM Architect)
└── gtm_architect_orchestrator.py (Strategie-Entwicklung)
├── gtm-architect/ (React Frontend)
└── server.cjs (Node.js API-Bridge)
VIII. DAS FUNDAMENT
└── config.py (Einstellungen & Konstanten für ALLE)
```
---
## 2. Steuerung & Ausführung (Control & Execution)
Diese Ebene bildet die Schnittstelle zur Außenwelt und startet die verschiedenen Prozesse.
### `app.py` (API Server)
#### Hauptfunktion
Das Modul `app.py` implementiert eine einfache Flask-Webanwendung, die als API-Endpunkt für das gesamte System dient. Es ermöglicht das Starten von langlaufenden Skripten (wie dem Duplikats-Check oder der Branchen-Neuklassifizierung) als asynchrone Hintergrundprozesse. Die Anwendung verwaltet diese Prozesse über eindeutige Job-IDs und bietet Endpunkte zum Starten von Aktionen und zum Abrufen des Status dieser Jobs. Dies ermöglicht eine lose Kopplung und die Steuerung des Systems durch externe Aufrufe, z.B. aus einem Frontend oder einem anderen Service.
#### Methodenbeschreibung
- `status_check()`: Ein einfacher `/status`-Endpunkt (GET), der eine "ok"-Nachricht zurückgibt. Dient als Health-Check, um zu überprüfen, ob die Flask-Anwendung läuft.
- `setup_ngrok()`: Eine Hilfsfunktion, die einen `ngrok`-Tunnel startet, um den lokalen Flask-Server über eine öffentliche URL erreichbar zu machen. Dies ist nützlich für das Testen und die Demonstration in Umgebungen ohne öffentliche IP.
- `run_script()`: Der Hauptendpunkt `/run-script` (POST), der eine Aktion entgegennimmt (z.B. "run_duplicate_check"). Basierend auf der Aktion wird das entsprechende Python-Skript (`duplicate_checker.py`, `brancheneinstufung2.py`, etc.) als separater Hintergrundprozess gestartet. Für jeden gestarteten Prozess wird eine eindeutige Job-ID generiert und eine initiale Statusdatei im `job_status`-Verzeichnis erstellt.
- `get_status()`: Ein `/get-status`-Endpunkt (GET), der den Inhalt aller im `job_status`-Verzeichnis gespeicherten JSON-Dateien ausliest. Dies ermöglicht es, den Fortschritt und den aktuellen Status aller gestarteten und abgeschlossenen Jobs abzufragen. Die Ergebnisse werden als JSON-Array zurückgegeben, wobei die neuesten Jobs zuerst aufgeführt sind.
### `brancheneinstufung2.py` (Orchestrator / Kommandozeile)
### Hauptfunktion
Das Skript `brancheneinstufung2.py` ist der zentrale Einstiegspunkt und Orchestrator für die Datenanreicherungs-Pipeline. Es dient nicht der direkten Implementierung der Verarbeitungslogik, sondern dem Parsen von Kommandozeilen-Argumenten, der Initialisierung von Handler-Klassen (`GoogleSheetHandler`, `WikipediaScraper`, `DataProcessor`, `SyncManager`) und dem Starten des vom Benutzer ausgewählten Verarbeitungsmodus. Das Skript ist hochgradig modular und delegiert die eigentliche Arbeit an die `DataProcessor`-Klasse.
### Abhängigkeiten
- **Standardbibliotheken**: `logging`, `os`, `argparse`, `time`, `datetime`.
- **Lokale Module**:
- `config.py`: Stellt globale Konfigurationsparameter und die Projektversion bereit.
- `helpers.py`: Bietet Hilfsfunktionen, z.B. für das Erstellen von Log-Dateinamen und das Initialisieren des Branchenschemas.
- `google_sheet_handler.py`: Kapselt die gesamte Kommunikation mit der Google Sheets API.
- `wikipedia_scraper.py`: Stellt Methoden zum Suchen und Parsen von Wikipedia-Artikeln bereit.
- `data_processor.py`: Enthält die Kernlogik für alle Datenverarbeitungsschritte.
- `sync_manager.py`: Implementiert die Logik zum Abgleich von CRM-Exporten mit dem Google Sheet.
### Wichtige Funktionen/Methoden
Die Logik des Skripts ist primär in der `main()`-Funktion enthalten und lässt sich in folgende Schritte unterteilen:
1. **Argumenten-Parsing**: Mittels `argparse` wird die Kommandozeile ausgewertet. Der wichtigste Parameter ist `--mode`, der den auszuführenden Prozess festlegt (z.B. `sync`, `full_run`, `reeval`, `predict_technicians`). Weitere Parameter wie `--limit` oder `--start_sheet_row` erlauben eine feingranulare Steuerung der zu verarbeitenden Datenmenge.
2. **Interaktiver Modus**: Wird kein `--mode` übergeben, startet ein interaktiver Modus, in dem der Benutzer aus einer Liste aller verfügbaren Modi auswählen kann.
3. **Initialisierung**: Basierend auf dem gewählten Modus werden die notwendigen Klassen instanziiert. Für die meisten Anreicherungs-Modi wird ein `DataProcessor`-Objekt erstellt, das wiederum Instanzen des `GoogleSheetHandler` und `WikipediaScraper` erhält. Für den `sync`-Modus wird der `SyncManager` verwendet.
4. **Modus-Dispatching**: Eine `if/elif/else`-Struktur leitet die Ausführung an die passende Methode weiter.
- **`sync` / `simulate_sync`**: Ruft den `SyncManager` auf, um einen D365-Excel-Export mit dem Google Sheet abzugleichen.
- **`full_run`**: Startet einen sequentiellen Anreicherungsprozess für Zeilen, die noch nicht verarbeitet wurden.
- **`reeval`**: Verarbeitet Zeilen, die explizit für eine Neubewertung markiert wurden.
- **`reclassify_branches`**: Führt eine Neubewertung der Branchenzuordnung für alle oder eine Teilmenge der Zeilen durch.
- **`train_technician_model` / `predict_technicians`**: Steuert das Training und die Anwendung eines Machine-Learning-Modells zur Vorhersage von Techniker-Anzahlen.
- **Dynamische Aufrufe**: Viele Modi (z.B. `wiki_verify`, `website_scraping`) werden dynamisch aufgerufen, indem Methoden des `DataProcessor`-Objekts ausgeführt werden, deren Namen dem Modus entsprechen (z.B. `process_wiki_verify`).
5. **Fehlerbehandlung und Logging**: Das gesamte Skript ist von einem `try...except...finally`-Block umschlossen, der Fehler abfängt, protokolliert und sicherstellt, dass das Logging am Ende sauber beendet wird.
## Kernlogik (data_processor.py)
Die Klasse `DataProcessor` ist das Herzstück der Anreicherungslogik und enthält alle Methoden zur schrittweisen Verarbeitung von Unternehmensdaten.
### `setup()`
- **Zweck:** Initialisiert den `DataProcessor`, indem das Ziel-Branchenschema geladen und das trainierte Machine-Learning-Modell für die Technikerschätzung in den Speicher geladen wird.
- **Input:** Keine direkten Argumente, greift aber auf die Konfigurationsdateien (`ziel_Branchenschema.csv`, `technician_decision_tree_model.pkl` etc.) zu.
- **Output:** Gibt `True` bei Erfolg zurück und setzt das interne Flag `is_setup_complete`. Bei Fehlern wird `False` zurückgegeben und das Skript beendet.
### `process_rows_sequentially(...)`
- **Zweck:** Führt den vollständigen, schrittweisen Anreicherungsprozess für einen definierten Bereich von Zeilen im Google Sheet aus. Dies ist der Hauptmodus für die initiale Verarbeitung neuer Daten.
- **Input:** `start_sheet_row` (Startzeile), `num_to_process` (Anzahl der Zeilen), sowie boolesche Flags (`process_wiki_steps`, `process_chatgpt_steps`, etc.) zur Steuerung der auszuführenden Anreicherungsschritte.
- **Output:** Die Methode hat keinen direkten Rückgabewert, aber ihr Seiteneffekt ist die Aktualisierung der verarbeiteten Zeilen im Google Sheet mit den angereicherten Daten.
### `process_reevaluation_rows(...)`
- **Zweck:** Führt den vollständigen Anreicherungsprozess gezielt für Zeilen aus, die im Google Sheet manuell mit einem 'x' in der "ReEval Flag"-Spalte markiert wurden. Vor der Neubewertung werden alle zuvor abgeleiteten Daten in der Zeile gelöscht.
- **Input:** `row_limit` (maximale Anzahl zu verarbeitender Zeilen), `clear_flag` (ob das 'x'-Flag nach der Verarbeitung entfernt werden soll) und die booleschen Flags zur Schritt-Steuerung.
- **Output:** Aktualisiert die markierten Zeilen im Google Sheet mit neu angereicherten Daten.
### `process_website_scraping(...)`
- **Zweck:** Führt einen Batch-Prozess ausschließlich für das Scrapen von Websites durch. Die Methode identifiziert Zeilen, in denen der "Website Scrape Timestamp" fehlt, und extrahiert parallel den Rohtext und die Meta-Details der jeweiligen Unternehmens-Websites.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit` zur Eingrenzung des Verarbeitungsbereichs.
- **Output:** Schreibt den extrahierten Rohtext, Meta-Details, einen Status zur URL-Prüfung und einen Zeitstempel in die entsprechenden Spalten im Google Sheet.
### `process_summarize_website(...)`
- **Zweck:** Führt einen Batch-Prozess zur Zusammenfassung von bereits extrahierten Website-Rohtexten mittels KI durch.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Die Methode verarbeitet Zeilen, bei denen der Rohtext vorhanden, aber die Zusammenfassung noch leer ist.
- **Output:** Schreibt die KI-generierte Zusammenfassung in die Spalte "Website Zusammenfassung" im Google Sheet.
### `process_branch_batch(...)` / `reclassify_all_branches(...)`
- **Zweck:** Führt eine (Neu-)Bewertung der Branchenzugehörigkeit für einen Zeilenbereich im Batch-Verfahren durch. Die Methode sammelt relevante Informationen (CRM-Daten, Website-Zusammenfassung, Wikipedia-Inhalte) und sendet sie gebündelt an die KI, um eine effiziente und konsistente Brancheneinstufung zu erhalten.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
- **Output:** Aktualisiert die Spalten zur KI-basierten Brancheneinstufung ("Chat Vorschlag Branche", Konfidenz, Begründung etc.) im Google Sheet.
### `process_wiki_verify(...)`
- **Zweck:** Verifiziert in einem Batch-Prozess, ob der in einer Zeile hinterlegte Wikipedia-Artikel thematisch zum Unternehmen passt. Nutzt KI, um die Konsistenz zu prüfen und ggf. einen passenderen Artikel vorzuschlagen.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die eine Wiki-URL, aber noch keinen Verifizierungs-Timestamp haben.
- **Output:** Schreibt das Ergebnis der KI-Prüfung (OK, X), eine Begründung und ggf. einen Alternativvorschlag in die entsprechenden Spalten im Google Sheet.
### `process_find_wiki_serp(...)`
- **Zweck:** Sucht für große Unternehmen (definiert über Mindestumsatz/-mitarbeiter), bei denen noch keine Wikipedia-URL bekannt ist, über eine Google-Suche (SerpAPI) nach einem passenden Wikipedia-Artikel.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`, `min_employees`, `min_umsatz`.
- **Output:** Wenn eine passende URL gefunden wird, wird diese in die "Wiki URL"-Spalte eingetragen und die Zeile für eine Neubewertung (`ReEval Flag`) markiert.
### `process_contact_search(...)`
- **Zweck:** Führt eine Suche nach LinkedIn-Kontakten für bestimmte Positionen (z.B. Serviceleiter, IT-Leiter) bei den Unternehmen im Sheet durch.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die noch keinen "Contact Search Timestamp" haben.
- **Output:** Schreibt die Anzahl der gefundenen Kontakte pro Kategorie in die "Linked... gefunden"-Spalten und speichert detaillierte Kontaktinformationen in einem separaten "Contacts"-Tabellenblatt.
### `train_technician_model()`
- **Zweck:** Bereitet die vorhandenen Daten auf, trainiert ein Machine-Learning-Modell (RandomForest) zur Vorhersage von Servicetechniker-Anzahlen, führt ein Hyperparameter-Tuning durch und speichert das trainierte Modell sowie den zugehörigen Imputer als `.pkl`-Dateien.
- **Input:** Keine direkten Argumente. Die Methode liest die Trainingsdaten aus dem konfigurierten Google Sheet.
- **Output:** Erstellt und speichert die `technician_decision_tree_model.pkl`- und `median_imputer.pkl`-Dateien sowie eine JSON-Datei mit den verwendeten Features.
### `process_predict_technicians(...)`
- **Zweck:** Wendet das trainierte ML-Modell auf Zeilen an, für die noch keine Techniker-Schätzung vorliegt, aber die notwendigen Input-Features (Umsatz, Mitarbeiter, Branche) vorhanden sind.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
- **Output:** Schreibt den vorhergesagten Techniker-Bucket (z.B. "Techniker_Klein (0-49)") in die Spalte "Geschaetzter Techniker Bucket".
---
## 3. Kern-Produktionslinie: Datenanreicherung
Dies ist das Herzstück der Fabrik, in dem die eigentliche Anreicherung der Unternehmensdaten stattfindet.
### `data_processor.py` (Der "Motor")
#### Hauptfunktion
Die Klasse `DataProcessor` ist das Herzstück der Anreicherungslogik und enthält alle Methoden zur schrittweisen Verarbeitung von Unternehmensdaten. Sie wird vom Orchestrator (`brancheneinstufung2.py`) aufgerufen und nutzt verschiedene Spezialisten-Module, um ihre Aufgaben zu erfüllen.
Die Klasse `DataProcessor` ist das Herzstück der Anreicherungslogik und enthält alle Methoden zur schrittweisen Verarbeitung von Unternehmensdaten.
### `setup()`
- **Zweck:** Initialisiert den `DataProcessor`, indem das Ziel-Branchenschema geladen und das trainierte Machine-Learning-Modell für die Technikerschätzung in den Speicher geladen wird.
- **Input:** Keine direkten Argumente, greift aber auf die Konfigurationsdateien (`ziel_Branchenschema.csv`, `technician_decision_tree_model.pkl` etc.) zu.
- **Output:** Gibt `True` bei Erfolg zurück und setzt das interne Flag `is_setup_complete`. Bei Fehlern wird `False` zurückgegeben und das Skript beendet.
### `process_rows_sequentially(...)`
- **Zweck:** Führt den vollständigen, schrittweisen Anreicherungsprozess für einen definierten Bereich von Zeilen im Google Sheet aus. Dies ist der Hauptmodus für die initiale Verarbeitung neuer Daten.
- **Input:** `start_sheet_row` (Startzeile), `num_to_process` (Anzahl der Zeilen), sowie boolesche Flags (`process_wiki_steps`, `process_chatgpt_steps`, etc.) zur Steuerung der auszuführenden Anreicherungsschritte.
- **Output:** Die Methode hat keinen direkten Rückgabewert, aber ihr Seiteneffekt ist die Aktualisierung der verarbeiteten Zeilen im Google Sheet mit den angereicherten Daten.
### `process_reevaluation_rows(...)`
- **Zweck:** Führt den vollständigen Anreicherungsprozess gezielt für Zeilen aus, die im Google Sheet manuell mit einem 'x' in der "ReEval Flag"-Spalte markiert wurden. Vor der Neubewertung werden alle zuvor abgeleiteten Daten in der Zeile gelöscht.
- **Input:** `row_limit` (maximale Anzahl zu verarbeitender Zeilen), `clear_flag` (ob das 'x'-Flag nach der Verarbeitung entfernt werden soll) und die booleschen Flags zur Schritt-Steuerung.
- **Output:** Aktualisiert die markierten Zeilen im Google Sheet mit neu angereicherten Daten.
### `process_website_scraping(...)`
- **Zweck:** Führt einen Batch-Prozess ausschließlich für das Scrapen von Websites durch. Die Methode identifiziert Zeilen, in denen der "Website Scrape Timestamp" fehlt, und extrahiert parallel den Rohtext und die Meta-Details der jeweiligen Unternehmens-Websites.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit` zur Eingrenzung des Verarbeitungsbereichs.
- **Output:** Schreibt den extrahierten Rohtext, Meta-Details, einen Status zur URL-Prüfung und einen Zeitstempel in die entsprechenden Spalten im Google Sheet.
### `process_summarize_website(...)`
- **Zweck:** Führt einen Batch-Prozess zur Zusammenfassung von bereits extrahierten Website-Rohtexten mittels KI durch.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Die Methode verarbeitet Zeilen, bei denen der Rohtext vorhanden, aber die Zusammenfassung noch leer ist.
- **Output:** Schreibt die KI-generierte Zusammenfassung in die Spalte "Website Zusammenfassung" im Google Sheet.
### `process_branch_batch(...)` / `reclassify_all_branches(...)`
- **Zweck:** Führt eine (Neu-)Bewertung der Branchenzugehörigkeit für einen Zeilenbereich im Batch-Verfahren durch. Die Methode sammelt relevante Informationen (CRM-Daten, Website-Zusammenfassung, Wikipedia-Inhalte) und sendet sie gebündelt an die KI, um eine effiziente und konsistente Brancheneinstufung zu erhalten.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
- **Output:** Aktualisiert die Spalten zur KI-basierten Brancheneinstufung ("Chat Vorschlag Branche", Konfidenz, Begründung etc.) im Google Sheet.
### `process_wiki_verify(...)`
- **Zweck:** Verifiziert in einem Batch-Prozess, ob der in einer Zeile hinterlegte Wikipedia-Artikel thematisch zum Unternehmen passt. Nutzt KI, um die Konsistenz zu prüfen und ggf. einen passenderen Artikel vorzuschlagen.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die eine Wiki-URL, aber noch keinen Verifizierungs-Timestamp haben.
- **Output:** Schreibt das Ergebnis der KI-Prüfung (OK, X), eine Begründung und ggf. einen Alternativvorschlag in die entsprechenden Spalten im Google Sheet.
### `process_find_wiki_serp(...)`
- **Zweck:** Sucht für große Unternehmen (definiert über Mindestumsatz/-mitarbeiter), bei denen noch keine Wikipedia-URL bekannt ist, über eine Google-Suche (SerpAPI) nach einem passenden Wikipedia-Artikel.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`, `min_employees`, `min_umsatz`.
- **Output:** Wenn eine passende URL gefunden wird, wird diese in die "Wiki URL"-Spalte eingetragen und die Zeile für eine Neubewertung (`ReEval Flag`) markiert.
### `process_contact_search(...)`
- **Zweck:** Führt eine Suche nach LinkedIn-Kontakten für bestimmte Positionen (z.B. Serviceleiter, IT-Leiter) bei den Unternehmen im Sheet durch.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die noch keinen "Contact Search Timestamp" haben.
- **Output:** Schreibt die Anzahl der gefundenen Kontakte pro Kategorie in die "Linked... gefunden"-Spalten und speichert detaillierte Kontaktinformationen in einem separaten "Contacts"-Tabellenblatt.
### `train_technician_model()`
- **Zweck:** Bereitet die vorhandenen Daten auf, trainiert ein Machine-Learning-Modell (RandomForest) zur Vorhersage von Servicetechniker-Anzahlen, führt ein Hyperparameter-Tuning durch und speichert das trainierte Modell sowie den zugehörigen Imputer als `.pkl`-Dateien.
- **Input:** Keine direkten Argumente. Die Methode liest die Trainingsdaten aus dem konfigurierten Google Sheet.
- **Output:** Erstellt und speichert die `technician_decision_tree_model.pkl`- und `median_imputer.pkl`-Dateien sowie eine JSON-Datei mit den verwendeten Features.
### `process_predict_technicians(...)`
- **Zweck:** Wendet das trainierte ML-Modell auf Zeilen an, für die noch keine Techniker-Schätzung vorliegt, aber die notwendigen Input-Features (Umsatz, Mitarbeiter, Branche) vorhanden sind.
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
- **Output:** Schreibt den vorhergesagten Techniker-Bucket (z.B. "Techniker_Klein (0-49)") in die Spalte "Geschaetzter Techniker Bucket".
### Spezialisten-Module
#### `google_sheet_handler.py` (Spezialist für Google Sheets)
##### Hauptfunktion
Das Modul `google_sheet_handler.py` dient als zentraler Wrapper für sämtliche Interaktionen mit dem Google Sheet, das als primärer Datenspeicher für das Projekt fungiert. Es abstrahiert die Komplexität der `gspread` API und stellt eine robuste, wiederverwendbare Schnittstelle für Lese- und Schreibvorgänge bereit. Die Klasse `GoogleSheetHandler` kapselt die Verbindungslogik, die Authentifizierung über Service-Accounts und bietet Methoden für spezifische Datenmanipulationen.
### Methodenbeschreibung
- `__init__(self, sheet_url=None)`: Der Konstruktor initialisiert den Handler. Er übernimmt optional eine `sheet_url`. Wenn keine URL angegeben wird, greift er auf den Wert aus der `config.py` zurück.
- `load_data(self)`: Stellt die Verbindung zum Google Sheet her (falls noch nicht geschehen) und lädt den gesamten Inhalt des Haupt-Arbeitsblatts in den internen Speicher der Klasse. Diese Methode ist mit einem Retry-Decorator versehen, um bei temporären Netzwerkproblemen robust zu sein.
- `get_sheet_as_dataframe(self, sheet_name)`: Liest ein spezifisches Arbeitsblatt (über `sheet_name` identifiziert) aus dem Google Sheet und gibt dessen Inhalt als Pandas DataFrame zurück. Dies ist nützlich für datenanalytische Aufgaben.
- `append_rows(self, sheet_name, values)`: Fügt eine oder mehrere neue Zeilen am Ende eines bestimmten Arbeitsblatts an. `values` ist dabei eine Liste von Listen (jede innere Liste repräsentiert eine Zeile).
- `clear_and_write_data(self, sheet_name, data)`: Löscht den gesamten Inhalt eines Arbeitsblatts und schreibt anschließend neue Daten hinein. Dies ist nützlich, um ein Sheet vollständig zu synchronisieren.
- `batch_update_cells(self, update_data)`: Führt eine Stapelverarbeitung von Zell-Updates durch. Diese Methode ist wesentlich performanter als einzelne Zell-Updates, da sie mehrere Änderungen in einer einzigen API-Anfrage bündelt.
- `get_main_sheet_name(self)`: Gibt den Namen des Haupt-Arbeitsblatts (typischerweise 'Tabelle1') zurück.
#### `wikipedia_scraper.py` (Spezialist für Wikipedia)
##### Hauptfunktion
Das Modul `wikipedia_scraper.py` kapselt alle Interaktionen mit Wikipedia. Seine Hauptaufgabe ist es, für ein gegebenes Unternehmen den relevantesten Wikipedia-Artikel zu finden, diesen zu validieren und anschließend strukturierte Daten wie den Unternehmenssitz, die Branche, den Umsatz und die Mitarbeiterzahl aus dem Artikel zu extrahieren. Es verwendet eine "Google-First"-Strategie, bei der die SerpAPI zur Identifizierung des wahrscheinlichsten Artikels genutzt wird, bevor eine detaillierte, faktenbasierte Validierung erfolgt.
###### Methodenbeschreibung
- `__init__(self, user_agent=None)`: Initialisiert den Scraper, setzt die Sprache für die `wikipedia`-Bibliothek (typischerweise 'de') und konfiguriert eine `requests.Session` mit einem benutzerdefinierten User-Agent für HTTP-Anfragen.
- `serp_wikipedia_lookup(self, company_name, lang='de')`: Nutzt die SerpAPI, um eine Google-Suche nach dem offiziellen Wikipedia-Artikel eines Unternehmens durchzuführen. Dies ist der erste und wichtigste Schritt, um einen Kandidaten-Artikel zu finden.
- `search_company_article(self, company_name, ...)`: Orchestriert den gesamten Such- und Validierungsprozess. Ruft zuerst `serp_wikipedia_lookup` auf, um eine URL zu erhalten. Anschließend wird der gefundene Artikel geladen und mit der internen Methode `_validate_article` auf Relevanz geprüft.
- `_validate_article(self, page, company_name, ...)`: Führt eine faktenbasierte Überprüfung eines Wikipedia-Artikels durch. Anstatt sich nur auf den Titel zu verlassen, prüft die Methode harte Kriterien wie die Übereinstimmung der Website-Domain (aus den Weblinks des Artikels) oder des Unternehmenssitzes (aus der Infobox).
- `extract_company_data(self, url_or_page)`: Die zentrale Extraktionsmethode. Nimmt eine URL oder ein `wikipedia.page`-Objekt entgegen und extrahiert daraus strukturierte Daten. Sie parst die Infobox des Artikels, um Werte für Branche, Umsatz, Mitarbeiter und Sitz zu finden, und extrahiert zusätzlich den Einleitungstext sowie die Kategorien.
- `_extract_infobox_value(self, soup, target)`: Eine interne Hilfsmethode, die gezielt nach Schlüsselwörtern (z.B. "Branche", "Umsatz") in der Infobox eines Artikels sucht und den zugehörigen Wert extrahiert und normalisiert.
- `_parse_sitz_string_detailed(self, raw_sitz_string_input)`: Eine spezialisierte Hilfsmethode, die versucht, aus dem oft unstrukturierten Textfeld für den Unternehmenssitz die Stadt und das Land zu trennen und zu normalisieren.
#### `sync_manager.py` (Spezialist für D365-Abgleich)
#### Hauptfunktion
Das Modul `sync_manager.py` ist für den robusten und intelligenten Datenabgleich zwischen einem D365 Excel-Export und dem Google Sheet verantwortlich. Es implementiert einen "Full-Sync"-Mechanismus, der neue, geänderte und potenziell zu archivierende Datensätze identifiziert. Der Manager stellt sicher, dass das Google Sheet die aktuellsten Informationen aus dem D365-Export widerspiegelt, wobei definierte Regeln für die Datenpriorisierung und Konfliktlösung angewendet werden.
#### Methodenbeschreibung
- `_normalize_text_for_comparison(self, text: str) -> str`: Eine interne Hilfsmethode, die Text normalisiert, um irrelevante Whitespace-Unterschiede zu ignorieren und so präzisere Vergleiche zu ermöglichen.
- `__init__(self, sheet_handler, d365_export_path)`: Der Konstruktor initialisiert den `SyncManager` mit einem `GoogleSheetHandler`-Objekt und dem Pfad zum D365 Excel-Export. Es definiert auch die Spaltenzuordnungen zwischen D365 und Google Sheet (`d365_to_gsheet_map`) sowie die Regeln, welche Spalten bei Konflikten Priorität haben (`d365_wins_cols`, `smart_merge_cols`).
- `_load_data(self)`: Lädt und bereitet die Daten aus dem D365 Excel-Export und dem Google Sheet vor. Diese Methode ist robust gegenüber "verschmutzten" Headern im Google Sheet und stellt sicher, dass beide Datensätze in einem konsistenten Format für den Abgleich vorliegen. Sie identifiziert auch neue und bestehende IDs.
- `run_sync(self)`: Orchestriert den gesamten Synchronisationsprozess. Nach dem Laden der Daten identifiziert es neue Accounts, aktualisiert bestehende Accounts gemäß den definierten Regeln (D365-Werte überschreiben GSheet-Werte für bestimmte Spalten, Smart-Merge für andere) und sammelt Statistiken über die durchgeführten Änderungen und Konflikte. Alle Änderungen werden in Batches an das Google Sheet gesendet.
- `debug_sync(self, debug_id=None)`: Bietet einen Debug-Modus für den Synchronisationsprozess. Ohne `debug_id` wird eine allgemeine Statistik über neue, bestehende und gelöschte IDs ausgegeben. Mit einer spezifischen `debug_id` führt es eine Tiefenanalyse für einen einzelnen Datensatz durch, zeigt Rohdaten und verarbeitete Daten und vergleicht kritische Felder.
- `simulate_sync(self, debug_id=None)`: Führt eine "Trockenlauf"-Simulation des Synchronisationsprozesses durch, ohne tatsächlich Daten im Google Sheet zu ändern. Es generiert einen detaillierten Bericht über alle potenziellen Änderungen, Updates und Konflikte, die im Falle eines echten Laufs auftreten würden. Dies ist nützlich zur Vorabprüfung und Fehleranalyse.
---
## 4. Marketing-Produktionslinie: Content-Erstellung
Diese Produktionslinie nutzt die angereicherten Daten, um direkt verwertbaren Marketing-Content zu erstellen.
### `generate_marketing_text.py` (Marketing Text Engine)
#### Hauptfunktion
Das Modul `generate_marketing_text.py` ist eine spezialisierte Engine zur automatischen Erstellung von hochgradig personalisierten Marketing-Texten. Es kombiniert eine strukturierte Wissensbasis (`marketing_wissen_final.yaml`) mit der Leistungsfähigkeit eines großen Sprachmodells (LLM), um für jede Kombination aus Zielbranche und Ansprechpartner-Position maßgeschneiderte Textbausteine für E-Mail-Kampagnen zu generieren. Das Skript ist so konzipiert, dass es nur neue, noch nicht existierende Textkombinationen erstellt und diese an ein Google Sheet anhängt, um die Effizienz zu maximieren.
#### Methodenbeschreibung
- `call_openai_with_retry(prompt, max_retries=3, delay=5)`: Eine robuste Wrapper-Funktion für Aufrufe an die OpenAI API. Sie implementiert eine Wiederholungslogik mit exponentiellem Backoff, um bei temporären API-Fehlern oder Netzwerkproblemen stabil zu bleiben.
- `build_prompt(branch_name, branch_data, position_name, position_data)`: Diese Funktion baut dynamisch den "Master-Prompt" für die KI zusammen. Sie integriert kontextbezogene Informationen wie die Herausforderungen (Pain Points) der Zielbranche und der spezifischen Ansprechpartner-Position. Eine wichtige Logik hierbei ist die dynamische Auswahl von Referenzkunden: Sind branchenspezifische Referenzen in der Wissensbasis vorhanden, werden diese verwendet; andernfalls greift die Funktion auf eine allgemeine Liste von Fallback-Referenzen zurück.
- `main(specific_branch=None)`: Die Haupt-Orchestrierungsfunktion des Skripts.
1. **Initialisierung:** Richtet das Logging ein, lädt API-Schlüssel und die Wissensbasis aus der YAML-Datei.
2. **Laden bestehender Texte:** Stellt eine Verbindung zum Google Sheet (`OUTPUT_SHEET_NAME`) her und lädt alle bereits generierten Textkombinationen, um doppelte Generierungen zu vermeiden.
3. **Generierungs-Loop:** Iteriert über alle möglichen Kombinationen von Branchen und Positionen aus der Wissensbasis.
4. **Überspringen-Logik:** Prüft für jede Kombination, ob sie bereits im Google Sheet vorhanden ist. Wenn ja, wird sie übersprungen.
5. **Text-Generierung:** Für neue Kombinationen wird der `build_prompt` aufgerufen, um den Prompt zu erstellen, und `call_openai_with_retry`, um die Textbausteine (Betreff, Einleitung, Referenz-Block) als JSON-Objekt zu generieren.
6. **Ergebnisse anhängen:** Alle neu generierten Texte werden gesammelt und am Ende des Prozesses in einem einzigen Batch-Aufruf an das Google Sheet angehängt, um die Anzahl der API-Aufrufe an Google zu minimieren.
---
## 5. ETL-Pipeline: Erstellung der Marketing-Wissensbasis
Dieser Bereich enthält alle Skripte, die als ETL-Pipelines (Extract, Transform, Load) dienen, um die zentrale Wissensbasis (`marketing_wissen.yaml`) aus verschiedenen Quellen zu erstellen und zu pflegen.
### `build_knowledge_base.py`
#### Hauptfunktion
Das Modul `build_knowledge_base.py` ist dafür verantwortlich, eine umfassende Wissensbasis für die Marketing-Text-Generierung zu erstellen. Es nutzt die in `config.py` definierten Brancheninformationen, um mittels KI für jede Branche ein detailliertes Dossier zu erstellen. Aus diesem Dossier werden dann strukturierte Daten wie eine Zusammenfassung, operative "Pain Points" und branchenspezifische Schlüsselbegriffe extrahiert. Das Endergebnis ist eine einzelne YAML-Datei (`marketing_wissen_final.yaml`), die als "Single Source of Truth" für die Textgenerierung dient.
#### Methodenbeschreibung
- `call_openai_with_retry(prompt, is_extraction=False, ...)`: Eine Wrapper-Funktion für OpenAI-API-Aufrufe, die eine Wiederholungslogik für den Fall von Fehlern implementiert. Sie kann sowohl für die Generierung von Freitext als auch für die Extraktion von strukturierten JSON-Daten konfiguriert werden.
- `generate_research_prompt(branch_name, branch_info)`: Erstellt einen Prompt für die KI, um ein detailliertes Branchen-Dossier zu generieren. Der Prompt wird mit Kontext aus der `config.py` angereichert, einschließlich der Branchendefinition und Beispielunternehmen, um eine hohe Relevanz sicherzustellen.
- `generate_extraction_prompt(dossier_content)`: Erstellt einen zweiten Prompt, der die KI anweist, aus dem zuvor generierten Dossier-Text strukturierte Informationen zu extrahieren. Der Fokus liegt hierbei auf operativen "Pain Points", die für den Außendienst relevant sind.
- `main(branches_to_process=None)`: Die Hauptfunktion, die den gesamten Prozess orchestriert:
1. **Initialisierung:** Lädt die API-Schlüssel und bereitet die Grundstruktur der Wissensbasis vor, einschließlich vordefinierter "Pain Points" für verschiedene Ansprechpartner-Positionen.
2. **Branchen-Selektion:** Verarbeitet entweder alle in `config.py` definierten Branchen oder eine spezifische Auswahl, die über Kommandozeilen-Argumente übergeben wird.
3. **Dossier-Generierung:** Für jede ausgewählte Branche wird `generate_research_prompt` aufgerufen und ein Dossier von der KI erstellt. Dieses wird zur Nachvollziehbarkeit als Textdatei im `industries`-Ordner gespeichert.
4. **Daten-Extraktion:** Das generierte Dossier wird verwendet, um mit `generate_extraction_prompt` die strukturierten Daten (Zusammenfassung, Pain Points, Schlüsselbegriffe) zu extrahieren.
5. **Zusammenführung:** Die extrahierten Daten werden zusammen mit den Referenzkunden aus der `config.py` in die Wissensbasis-Struktur eingefügt.
6. **Speichern:** Die vollständige, angereicherte Wissensbasis wird am Ende des Prozesses in die finale YAML-Datei (`marketing_wissen_final.yaml`) geschrieben.
### `expand_knowledge_base.py`
#### Hauptfunktion
Das Modul `expand_knowledge_base.py` dient dazu, eine bestehende Wissensbasis (`marketing_wissen.yaml`) gezielt zu erweitern. Es identifiziert, welche Branchen aus der zentralen Konfiguration (`config.py`) noch in der Wissensbasis fehlen, und generiert für diese fehlenden Branchen die entsprechenden Einträge. Der Prozess ist identisch mit dem von `build_knowledge_base.py`: Es wird ein KI-gestütztes Dossier erstellt, aus dem dann strukturierte Daten extrahiert und in die Wissensbasis integriert werden. Das Ergebnis wird in einer neuen, kompletten Datei (`marketing_wissen_komplett.yaml`) gespeichert.
#### Methodenbeschreibung
- `call_openai_with_retry(prompt, is_extraction=False, ...)`: Eine Wrapper-Funktion für OpenAI-API-Aufrufe mit Wiederholungslogik, die sowohl Freitext-Generierung als auch JSON-Extraktion unterstützt.
- `generate_research_prompt(branch_name)`: Erstellt einen Prompt für die KI, um ein Branchen-Dossier zu einem gegebenen Branchennamen zu erstellen.
- `generate_extraction_prompt(dossier_content)`: Erstellt einen Prompt, um aus einem generierten Dossier-Text strukturierte Daten (Zusammenfassung, Pain Points, Schlüsselbegriffe) im JSON-Format zu extrahieren.
- `main(branches_to_process=None)`: Die Hauptfunktion, die den Erweiterungsprozess steuert:
1. **Initialisierung:** Lädt API-Schlüssel und die existierende Basis-Wissensdatei (`marketing_wissen.yaml`).
2. **Delta-Ermittlung:** Vergleicht die Liste aller Branchen aus `config.py` mit den bereits in der Wissensbasis vorhandenen Branchen, um die Liste der zu bearbeitenden, fehlenden Branchen zu ermitteln.
3. **Gezielte Verarbeitung:** Iteriert ausschließlich über die fehlenden Branchen (oder eine über Kommandozeilen-Argumente spezifizierte Teilmenge).
4. **Dossier-Generierung & Extraktion:** Führt für jede neue Branche den zweistufigen Prozess aus: Zuerst wird das Dossier generiert und als Textdatei gespeichert, danach werden die strukturierten Daten extrahiert.
5. **Aktualisierung:** Fügt die neu extrahierten Daten zur in-memory-Version der Wissensbasis hinzu.
6. **Speichern:** Schreibt die erweiterte und nun vollständige Wissensbasis in eine neue Zieldatei (`marketing_wissen_komplett.yaml`).
### `extract_insights.py`
#### Hauptfunktion
Das Modul `extract_insights.py` ist ein Werkzeug zur automatisierten Erstellung einer Wissensbasis aus unstrukturierten Word-Dokumenten (`.docx`). Es liest Branchenanalysen aus einem spezifizierten Ordner, sendet deren Inhalt an eine KI und extrahiert gezielt strukturierte Informationen wie operative "Pain Points", branchenspezifische Fachbegriffe und eine Management-Zusammenfassung. Diese extrahierten Daten werden in einer einzigen, strukturierten YAML-Datei (`marketing_wissen_v1.yaml`) zusammengefasst, die als Grundlage für weitere Marketing-Automatisierungen dient.
#### Methodenbeschreibung
- `call_openai_with_retry(prompt, ...)`: Eine robuste Wrapper-Funktion für OpenAI-API-Aufrufe, die eine Wiederholungslogik bei Fehlern implementiert.
- `read_docx_content(filepath)`: Eine Hilfsfunktion, die eine `.docx`-Datei einliest und deren gesamten Textinhalt, einschließlich Absätzen und Tabellen, als einzelnen String zurückgibt.
- `extract_yaml_from_response(response_text)`: Eine wichtige Bereinigungsfunktion, die sicherstellt, dass aus der oft mit Markdown-Formatierungen (` ```yaml ... ``` `) versehenen KI-Antwort nur der reine YAML-Code extrahiert wird, um Parsing-Fehler zu vermeiden.
- `generate_extraction_prompt(content, data_to_extract)`: Erstellt hochspezialisierte Prompts für die KI. Je nachdem, welche Information extrahiert werden soll (`pain_points`, `key_terms` oder `summary`), wird der KI eine andere Rolle und ein anderer Auftrag zugewiesen, um die Qualität und Relevanz der extrahierten Daten zu maximieren.
- `main()`: Die Hauptfunktion, die den gesamten ETL-Prozess (Extract, Transform, Load) steuert:
1. **Initialisierung:** Lädt die API-Schlüssel und prüft, ob der Quellordner mit den Word-Dokumenten existiert.
2. **Dokumenten-Loop:** Iteriert über alle `.docx`-Dateien im Quellordner.
3. **Text-Extraktion:** Liest den Inhalt jedes Dokuments mit `read_docx_content`.
4. **Iterative KI-Extraktion:** Führt für jedes Dokument drei separate KI-Aufrufe durch (einen für "Pain Points", einen für "Key Terms" und einen für "Summary"), um die Genauigkeit zu erhöhen.
5. **Daten-Aggregation:** Sammelt die extrahierten und geparsten YAML-Daten für jede Branche in einer zentralen `knowledge_base`-Struktur.
6. **Speichern:** Schreibt die finale, aggregierte Wissensbasis in die `marketing_wissen_v1.yaml`-Datei.
### `generate_knowledge_base.py`
#### Hauptfunktion
Das Modul `generate_knowledge_base.py` ist ein KI-gestütztes Skript zur Erstellung eines ersten Entwurfs für eine Marketing-Wissensbasis (`marketing_wissen_entwurf.yaml`). Es generiert zwei Kernbestandteile:
1. **Branchen-Pain-Points:** Für eine vordefinierte Liste von Fokusbranchen werden die spezifischen operativen Herausforderungen im Außendienst identifiziert.
2. **Positions-Fokus:** Für eine Liste von typischen Ansprechpartner-Positionen wird deren jeweiliger strategischer Fokus in Bezug auf Serviceprozesse formuliert.
Das Skript nutzt spezialisierte Prompts, um die KI in die Rolle eines Branchenexperten oder Vertriebs-Coaches zu versetzen und so qualitativ hochwertige, relevante Inhalte zu generieren. Das Ergebnis dient als Grundlage, die manuell überprüft und verfeinert werden kann.
#### Methodenbeschreibung
- `call_openai_with_retry(prompt, ...)`: Eine Standard-Wrapper-Funktion für OpenAI-API-Aufrufe mit integrierter Wiederholungslogik, um die Stabilität bei Netzwerk- oder API-Problemen zu gewährleisten.
- `generate_pain_points_prompt(branch_name)`: Erstellt einen detaillierten Prompt, der die KI anweist, sich in die Rolle eines Top-Strategieberaters zu versetzen. Der Prompt enthält einen "Chain of Thought"-Abschnitt, der die KI anleitet, über typische Aufgaben und Probleme im Außendienst der jeweiligen Branche nachzudenken, bevor sie die finalen "Pain Points" formuliert. Das Ausgabeformat wird strikt als YAML-Liste vorgegeben.
- `generate_position_focus_prompt(position_name)`: Erstellt einen Prompt, der die KI als erfahrenen B2B-Vertriebs-Coach positioniert. Die Aufgabe ist es, einen einzigen, prägnanten Satz zu formulieren, der den Hauptfokus einer bestimmten Ansprechpartner-Rolle (z.B. CFO, IT-Leiter) zusammenfasst.
- `main()`: Die Hauptfunktion, die den gesamten Generierungsprozess steuert:
1. **Initialisierung:** Lädt die API-Schlüssel und definiert die zu bearbeitenden Fokusbranchen und Positionen.
2. **Branchen-Verarbeitung:** Iteriert durch die Liste der `FOKUS_BRANCHEN`, ruft für jede Branche `generate_pain_points_prompt` auf und sendet den Prompt an die KI. Die Antwort wird geparst und in der `knowledge_base`-Struktur gespeichert.
3. **Positions-Verarbeitung:** Iteriert durch die Liste der `POSITIONEN`, generiert mit `generate_position_focus_prompt` den entsprechenden Prompt und lässt die KI den Fokus-Satz formulieren.
4. **Speichern:** Schreibt die gesammelten Daten in die Zieldatei `marketing_wissen_entwurf.yaml`, die als Arbeitsgrundlage für die finale Wissensbasis dient.
---
## 6. Sub-System: Kontakt-Klassifizierung
Dies ist ein eigenständiges System innerhalb des Projekts, das sich ausschließlich mit der Analyse und Kategorisierung von Job-Titeln befasst.
### `contact_grouping.py` (Klassifizierer)
#### Hauptfunktion
Das Modul `contact_grouping.py` ist für die automatische Klassifizierung von Jobtiteln in vordefinierte Abteilungen zuständig. Es nutzt eine mehrstufige Logik, die auf einer Kombination aus regelbasierten Mappings (exakte Treffer und Keyword-Regeln) und einer KI-gestützten Klassifizierung basiert. Das Ziel ist es, die Jobtitel von Kontakten aus einem Google Sheet (`Matching_Positions`) einer passenden Abteilung zuzuordnen, wobei auch der Unternehmensbranche-Kontext berücksichtigt wird.
#### Methodenbeschreibung
- `__init__(self)`: Initialisiert die `ContactGrouper`-Klasse und bereitet die internen Variablen für die Wissensbasis vor.
- `load_knowledge_base(self)`: Lädt die zuvor erstellten Wissensbasis-Dateien (`exact_match_map.json` und `keyword_rules.json`) in den Speicher. Diese Dateien enthalten die Regeln für die regelbasierte Zuordnung. Außerdem generiert es Beispiele für den KI-Prompt aus der geladenen Wissensbasis.
- `_load_json(self, file_path)`: Eine interne Hilfsmethode zum sicheren Laden und Parsen von JSON-Dateien.
- `_normalize_text(self, text)`: Eine interne Hilfsmethode zur Normalisierung von Texten (Kleinschreibung, Leerzeichen entfernen).
- `_generate_ai_examples(self)`: Generiert einen Teil des KI-Prompts, der Beispiele für typische Jobtitel pro Abteilung enthält. Dies hilft der KI, die Klassifizierungsaufgabe besser zu verstehen.
- `_find_best_match(self, job_title, company_branch)`: Die Kernlogik für die regelbasierte Zuordnung. Sie versucht zuerst einen exakten Match des `job_title` zu finden. Wenn dies fehlschlägt, werden Keyword-Regeln angewendet, wobei die `company_branch` zur Verfeinerung der Zuordnung genutzt wird. Prioritäten der Abteilungen werden bei mehreren Treffern berücksichtigt.
- `_get_ai_classification(self, contacts_to_classify)`: Sendet eine Liste von Jobtiteln, die nicht regelbasiert zugeordnet werden konnten, an die OpenAI API zur KI-gestützten Klassifizierung. Der Prompt enthält die gültigen Abteilungen und generierte Beispiele, um die KI-Antwort zu steuern. Die Ergebnisse werden als Dictionary zurückgegeben.
- `_append_learnings_to_source(self, gsh, new_mappings_df)`: Hängt neue, von der KI erfolgreich klassifizierte Jobtitel und deren Abteilungen an das Quell-Sheet (`CRM_Jobtitles`) an, um die Wissensbasis kontinuierlich zu erweitern.
- `process_contacts(self)`: Die Hauptmethode, die den gesamten Kontakt-Klassifizierungsprozess steuert:
1. Lädt die Kontaktdaten aus dem `TARGET_SHEET_NAME` Google Sheet.
2. Führt die regelbasierte Zuordnung (`_find_best_match`) für alle Jobtitel durch.
3. Identifiziert Jobtitel, die nicht regelbasiert zugeordnet werden konnten (`DEFAULT_DEPARTMENT`).
4. Sendet diese unklassifizierten Jobtitel in Batches an die KI (`_get_ai_classification`) zur weiteren Klassifizierung.
5. Aktualisiert die `Department`-Spalte im DataFrame mit den KI-Ergebnissen.
6. Hängt neue KI-Erkenntnisse an das Lern-Quell-Sheet an (`_append_learnings_to_source`).
7. Schreibt die finalen, klassifizierten Daten zurück in das `TARGET_SHEET_NAME` Google Sheet.
### `knowledge_base_builder.py` (Wissensbasis-Ersteller für Klassifizierung)
#### Hauptfunktion
Das Modul `knowledge_base_builder.py` ist dafür verantwortlich, eine Wissensbasis für die automatische Zuordnung von Jobtiteln zu Abteilungen zu erstellen. Es verarbeitet eine Liste von Jobtiteln und deren zugehörigen Abteilungen und Branchen aus einem Google Sheet. Das Skript generiert zwei Haupt-Artefakte:
1. **Exaktes Mapping (`exact_match_map.json`):** Eine einfache Zuordnung von exakten, normalisierten Jobtiteln zu ihrer dominantesten Abteilung.
2. **Keyword-Regeln (`keyword_rules.json`):** Eine Sammlung von Regeln, die Schlüsselwörter pro Abteilung identifiziert und optional branchenspezifische Anforderungen für die Zuordnung hinzufügen, basierend auf der Häufigkeit von Branchen innerhalb der Abteilung.
Das Ziel ist es, die automatische und präzise Klassifizierung neuer, unbekannter Jobtitel zu ermöglichen.
#### Methodenbeschreibung
- `setup_logging()`: Eine Hilfsfunktion, die das Logging für das Skript konfiguriert und sicherstellt, dass alle Ausgaben sowohl in eine Datei als auch in die Konsole geschrieben werden.
- `build_knowledge_base()`: Dies ist die Hauptfunktion des Moduls, die den gesamten Prozess der Wissensbasiserstellung orchestriert:
1. **Daten laden:** Stellt eine Verbindung zum konfigurierten Google Sheet (definiert in `SOURCE_SHEET_NAME`) her und lädt die Daten in einen Pandas DataFrame.
2. **Datenbereinigung:** Entfernt Zeilen mit fehlenden oder leeren Jobtiteln/Abteilungen und normalisiert die Jobtitel für konsistente Vergleiche.
3. **Exaktes Mapping:** Gruppiert die Jobtitel nach ihrer normalisierten Form und ermittelt die am häufigsten (modal) vorkommende Abteilung, um eine direkte 1:1-Zuordnung zu erstellen. Das Ergebnis wird als JSON-Datei (`exact_match_map.json`) gespeichert.
4. **Keyword-Regeln erstellen:** Für jede Abteilung werden die häufigsten und aussagekräftigsten Schlüsselwörter aus den zugehörigen Jobtiteln extrahiert. Dabei werden eine vordefinierte Liste von Stoppwörtern und generischen Begriffen ignoriert. Diese Regeln erhalten eine Priorität basierend auf `DEPARTMENT_PRIORITIES`.
5. **Branchenspezifische Anpassung:** Optional wird geprüft, ob eine Abteilung stark mit einer bestimmten Branchengruppe (definiert in `BRANCH_GROUP_RULES`) korreliert. Wenn eine hohe Spezifität (> `BRANCH_SPECIFICITY_THRESHOLD`) und genügend Datenpunkte vorhanden sind, wird diese Branchenanforderung zur Keyword-Regel hinzugefügt. Das Ergebnis wird als JSON-Datei (`keyword_rules.json`) gespeichert.
6. **Fehlerbehandlung:** Robuste Fehlerbehandlung für Datei-I/O und Datenladen aus Google Sheets.
---
## 7. Standalone-Werkzeuge
Dieser Bereich enthält Skripte, die als eigenständige Werkzeuge für spezifische Aufgaben konzipiert sind.
### `company_deduplicator.py` (Duplikats-Check)
#### Hauptfunktion
Das Skript `company_deduplicator.py` (ehemals `duplicate_checker_old.py`) ist ein spezialisiertes Werkzeug zur Identifizierung von potenziellen Unternehmens-Duplikaten. Es operiert in zwei Modi:
1. **Externer Vergleich:** Identifiziert Duplikate zwischen einer externen Liste (`Matching_Accounts`) und der internen CRM-Liste (`CRM_Accounts`). Dies ist die ursprüngliche Funktionalität.
2. **Interne Deduplizierung:** Findet Duplikate *innerhalb* der `CRM_Accounts`-Liste, gruppiert diese und markiert sie zur weiteren Bearbeitung.
Es verwendet einen gewichteten, heuristischen Algorithmus, um Ähnlichkeiten zu bewerten und nutzt bekannte Unternehmenshierarchien (`Parent Account`), um Falsch-Positive zu reduzieren.
### Meeting Assistant (Transcription Tool)
Ein lokaler Micro-Service zur Transkription und Analyse von Audio-Dateien (Meetings, Calls, Interviews). Nutzt Gemini 2.0 Flash für kostengünstige, hochqualitative Ergebnisse.
* **Dokumentation:** [TRANSCRIPTION_TOOL.md](TRANSCRIPTION_TOOL.md)
* **Funktionen:** Upload (MP3/WAV), Automatisches Chunking (FFmpeg), Transkription mit Timestamps.
* **Zugriff:** Über das Dashboard `/tr/`.
### `import_competitive_radar.py` (Competitor Import)
#### Hauptfunktion
Dieses Skript dient dem einmaligen Import von Wettbewerbsanalyse-Daten aus einer JSON-Datei in die Notion-Datenbanken. Es liest eine spezifische JSON-Datei (`analysis_robo-planet.de-4.json`), die die Ergebnisse einer externen Analyse enthält, und legt die entsprechenden Einträge für Unternehmen, Produkte und deren Verknüpfungen in Notion an. Das Skript ist zustandsbewusst, d.h. es prüft vor dem Anlegen, ob ein Eintrag bereits existiert, um Duplikate zu vermeiden.
#### Neue Features (Interne Deduplizierung)
- **Zwei-Modi-Betrieb:** Das Skript fragt beim Start interaktiv ab, ob ein externer Vergleich oder eine interne Deduplizierung durchgeführt werden soll.
- **Gruppierung & ID-Zuweisung:** Im internen Modus werden gefundene Duplikatspaare zu Clustern zusammengefasst (z.B. wenn A=B und B=C, dann ist A,B,C eine Gruppe). Jede Gruppe erhält eine eindeutige ID (z.B. `Dup_0001`), die in eine neue Spalte `Duplicate_ID` im `CRM_Accounts`-Sheet geschrieben wird.
- **Nutzung von Unternehmenshierarchien:** Das Skript liest die Spalte `Parent Account` aus dem `CRM_Accounts`-Sheet. Bekannte Eltern-Kind-Beziehungen werden automatisch von der Duplikatsprüfung ausgeschlossen, was die Genauigkeit erheblich verbessert.
- **Handlungsempfehlungen:** Findet das Skript eine Duplikatsgruppe, in der bei keinem der Mitglieder ein `Parent Account` gepflegt ist, wird dies in einer neuen Spalte `Duplicate_Hint` vermerkt. Dies dient als Hinweis, dass hier möglicherweise eine Hierarchie im CRM nachgepflegt werden sollte.
#### Abhängigkeiten
- **`pandas`**: Zur Datenmanipulation und -analyse.
- **`thefuzz`**: Für Fuzzy-String-Vergleiche zur Bewertung der Namensähnlichkeit.
- **`gspread`, `oauth2client`**: Für die Interaktion mit der Google Sheets API.
- **Lokale Module**:
- `helpers.py`: Stellt Normalisierungsfunktionen (`normalize_company_name`, `simple_normalize_url`) und eine optionale Web-Suche (`serp_website_lookup`) bereit.
- `config.py`: Zentralisiert Konfigurationswerte und API-Schlüssel.
- `google_sheet_handler.py`: Kapselt die Logik für den Zugriff auf Google Sheets.
#### Konfiguration
Die Logik des Skripts wird durch mehrere Konstanten am Anfang der Datei gesteuert:
- **Sheet-Namen**: `CRM_SHEET_NAME` und `MATCHING_SHEET_NAME`.
- **Score-Schwellenwerte**: `SCORE_THRESHOLD` (Standard: 80) und `SCORE_THRESHOLD_WEAK` (95), ein strengerer Wert für Treffer ohne starke Indikatoren wie Domain- oder Standortübereinstimmung.
- **Penalties**: `CITY_MISMATCH_PENALTY` (30) und `COUNTRY_MISMATCH_PENALTY` (40) werden vom Score abgezogen, wenn Standorte voneinander abweichen.
- **Prefiltering**: `PREFILTER_MIN_PARTIAL` (70) und `PREFILTER_LIMIT` (30) zur Vorauswahl potenzieller Kandidaten, um die Performance zu verbessern.
- **Stop-Wörter**: `STOP_TOKENS_BASE` und `CITY_TOKENS` (dynamisch generiert) enthalten Begriffe, die bei Namensvergleichen ignoriert werden, um die Signalqualität zu erhöhen.
#### Ablauf-Logik (Gekürzt)
1. **Modus-Auswahl**: Das Skript fragt den Benutzer nach dem gewünschten Modus (Extern vs. Intern).
2. **Initialisierung & Daten laden**: Richtet das Logging ein und lädt die relevanten Google Sheets. Im internen Modus wird auch die `Parent Account`-Spalte geladen.
3. **Normalisierung & Index-Erstellung**: Unternehmensnamen, Domains, Orte und Parent Accounts werden standardisiert. Zur Optimierung der Suche werden "Blocking"-Indizes (z.B. für Domains und Namens-Tokens) erstellt.
4. **Matching-Prozess**:
- **Extern:** Jeder Eintrag aus `Matching_Accounts` wird mit Kandidaten aus `CRM_Accounts` verglichen.
- **Intern:** Jeder Eintrag aus `CRM_Accounts` wird mit nachfolgenden Kandidaten aus derselben Liste verglichen, wobei bekannte Hierarchien übersprungen werden.
5. **Scoring & Gruppierung**: Kandidatenpaare werden bewertet. Im internen Modus werden Paare über dem Schwellenwert zu Gruppen zusammengefasst.
6. **Daten zurückschreiben**:
- **Extern:** Die Ergebnisse (`Match`, `Score`) werden in das `Matching_Accounts`-Sheet geschrieben.
- **Intern:** Die `Duplicate_ID` und `Duplicate_Hint` werden in neue Spalten im `CRM_Accounts`-Sheet geschrieben.
#### To-Dos & Nächste Schritte
- **Scoring-Schwellenwert erhöhen:** Der `SCORE_THRESHOLD` von 80 ist für die interne Deduplizierung zu niedrig und sollte auf 95 oder höher angehoben werden, um Falsch-Positive zu reduzieren.
- **Stop-Wort-Liste erweitern:** Die `STOP_TOKENS_BASE`-Liste um generische Branchenbegriffe wie 'energy', 'services', 'group', 'solutions' etc. erweitern.
- **Scoring-Logik anpassen:** Die Gewichtung sollte angepasst werden, sodass eine hohe Namensähnlichkeit in Kombination mit einem Standort-Match stärker bewertet wird.
- **Debugging-Log verbessern:** Die Log-Ausgabe für gefundene Paare sollte die komplette Scoring-Zusammensetzung ausgeben, um die Analyse von Ergebnissen zu erleithern.
---
## 8. Projekt-Fundament
Diese Module stellen grundlegende Funktionen und Konfigurationen für das gesamte Projekt bereit.
### `config.py` (Zentrale Konfiguration)
#### Hauptfunktion
Das Modul `config.py` dient als zentrale Konfigurationsdatei für das gesamte Projekt "Automatisierte Unternehmensbewertung". Es bündelt alle globalen Einstellungen, Dateipfade, API-Schlüssel-Pfade, Schwellenwerte und Mappings, die von verschiedenen Modulen im Projekt verwendet werden. Durch die Zentralisierung der Konfiguration wird die Wartbarkeit und Anpassbarkeit des Systems erheblich verbessert.
#### Methodenbeschreibung
- `normalize_for_mapping(text)`: Eine Hilfsfunktion, die einen String aggressiv für Mapping-Zwecke normalisiert, indem er in Kleinbuchstaben umgewandelt, getrimmt und alle nicht-alphanumerischen Zeichen entfernt werden. Diese Funktion wird intern von der `Config`-Klasse verwendet.
- `Config` Klasse:
- **Attribute:** Enthält statische Attribute wie `VERSION`, `LANG`, `SHEET_URL`, `MAX_RETRIES`, `RETRY_DELAY`, `REQUEST_TIMEOUT`, `SIMILARITY_THRESHOLD`, `DEBUG`, `WIKIPEDIA_SEARCH_RESULTS`, `HTML_PARSER`, `TOKEN_MODEL`, `USER_AGENT`.
- **Batching & Parallelisierung:** Konfigurationen für die Batch-Verarbeitung und Parallelisierung, z.B. `PROCESSING_BATCH_SIZE`, `OPENAI_BATCH_SIZE_LIMIT`, `MAX_SCRAPING_WORKERS`.
- **Plausibilitäts-Schwellenwerte:** Definiert numerische Schwellenwerte für Plausibilitätsprüfungen von Umsatz- und Mitarbeiterzahlen, z.B. `PLAUSI_UMSATZ_MIN_WARNUNG`, `PLAUSI_RATIO_UMSATZ_PRO_MA_MIN`.
- **Länder-Codes Mapping (`COUNTRY_CODE_MAP`):** Ein Dictionary, das D365-Ländercodes in die im Google Sheet verwendeten Langformen übersetzt.
- **Branchen-Gruppen Mapping (`BRANCH_GROUP_MAPPING`):** Eine "Single Source of Truth" für alle Branchen, angereichert mit Definitionen, Beispielen und D365-Branch-Details.
- **API-Schlüssel (`API_KEYS`):** Ein Dictionary, das die geladenen API-Schlüssel speichert.
- `load_api_keys()` (Klassenmethode): Lädt API-Schlüssel aus den konfigurierten Dateipfaden (`API_KEY_FILE`, `SERP_API_KEY_FILE`, `GENDERIZE_API_KEY_FILE`) und setzt den OpenAI API-Schlüssel global.
- `_load_key_from_file(filepath)` (Statische Methode): Eine interne Hilfsfunktion zum sicheren Laden eines API-Schlüssels aus einer angegebenen Datei.
- `COLUMN_ORDER`: Eine globale Liste, die die exakte und garantierte Reihenfolge aller Spalten im Google Sheet definiert. Dies dient als "Single Source of Truth" für alle Index-Berechnungen.
- `COLUMN_MAP`: Ein globales Dictionary, das detaillierte Mappings für jede Spalte im Google Sheet bereitstellt, einschließlich des Spaltentitels (z.B. "A", "B") und des 0-basierten Index.
- **DEALFRONT AUTOMATION CONFIGURATION:** Spezifische Konfigurationen für die Dealfront-Automatisierung, einschließlich `DEALFRONT_CREDENTIALS_FILE`, `DEALFRONT_LOGIN_URL`, `DEALFRONT_TARGET_URL` und `TARGET_SEARCH_NAME`.
### `helpers.py` (Globaler Werkzeugkasten)
Diese Datei enthält eine Sammlung von globalen, wiederverwendbaren Hilfsfunktionen, die in verschiedenen Modulen des Projekts verwendet werden.
#### Decorators
##### `retry_on_failure(func)`
- **Zweck:** Ein Decorator, der eine Funktion bei bestimmten, temporären Fehlern (z.B. Netzwerkprobleme, API-Rate-Limits) automatisch mehrmals ausführt. Er verwendet eine exponentielle Backoff-Strategie, um die Wartezeit zwischen den Versuchen zu erhöhen.
- **Input:** Eine Funktion, die dekoriert werden soll.
- **Output:** Das Ergebnis der dekorierten Funktion oder löst eine Ausnahme aus, wenn alle Wiederholungsversuche fehlschlagen.
#### Logging & Token Counting
##### `token_count(text, model=None)`
- **Zweck:** Zählt die Anzahl der Tokens in einem gegebenen Text. Verwendet die `tiktoken`-Bibliothek für eine genaue Zählung, falls verfügbar, andernfalls schätzt sie die Anzahl basierend auf Leerzeichen.
- **Input:** `text` (der zu analysierende String), `model` (optional, das zu verwendende KI-Modell).
- **Output:** Eine Ganzzahl, die die Anzahl der Tokens darstellt.
##### `create_log_filename(mode)`
- **Zweck:** Erstellt einen standardisierten, zeitgestempelten Dateinamen für Log-Dateien.
- **Input:** `mode` (ein String, der den aktuellen Ausführungsmodus beschreibt, z.B. 'full_run').
- **Output:** Ein String, der den vollständigen Pfad zur Log-Datei enthält (z.B. `Log/2025-11-07_10-30_v221_Modus-full_run.txt`).
#### Text-, String- & URL-Utilities
##### `simple_normalize_url(url)`
- **Zweck:** Bereinigt und normalisiert eine URL auf ihre Kern-Domain (z.B. `https://www.beispiel.de/path` -> `beispiel.de`).
- **Input:** `url` (ein String mit einer URL).
- **Output:** Ein normalisierter Domain-String oder "k.A." bei ungültiger Eingabe.
##### `normalize_string(s)`
- **Zweck:** Standardisiert einen String, indem Umlaute (ä -> ae) und gängige Sonderzeichen ersetzt werden.
- **Input:** `s` (ein beliebiger String).
- **Output:** Der normalisierte String.
##### `clean_text(text)`
- **Zweck:** Bereinigt einen Text von unerwünschten Artefakten, die typischerweise beim Scrapen von Websites oder Wikipedia auftreten (z.B. `[1]`, `[Bearbeiten]`, überflüssige Leerzeichen).
- **Input:** `text` (ein String).
- **Output:** Der bereinigte Text.
##### `normalize_company_name(name)`
- **Zweck:** Normalisiert einen Firmennamen, indem gängige Rechtsformzusätze (GmbH, AG, etc.) und andere generische Begriffe entfernt werden, um Vergleiche zu erleichtern.
- **Input:** `name` (ein Firmenname als String).
- **Output:** Der normalisierte Firmenname in Kleinbuchstaben.
##### `extract_numeric_value(raw_value, is_umsatz=False)`
- **Zweck:** Extrahiert und normalisiert einen numerischen Wert aus einem unstrukturierten String. Kann Einheiten wie "Mio.", "Mrd." oder "Tsd." interpretieren und in einen Basiswert umrechnen.
- **Input:** `raw_value` (ein String, z.B. "ca. 250 Mio. EUR"), `is_umsatz` (ein Flag, das die Umrechnung für Umsätze steuert, z.B. Rückgabe in Millionen).
- **Output:** Ein String mit der normalisierten Zahl (z.B. "250") oder "k.A.".
#### API Wrappers & Externe Dienste
##### `call_openai_chat(...)`
- **Zweck:** Eine zentrale Wrapper-Funktion für Aufrufe an die OpenAI Chat-API. Kapselt die Authentifizierung, die Fehlerbehandlung und den eigentlichen API-Aufruf.
- **Input:** `prompt` (der an die KI gesendete Text), `temperature` (steuert die Kreativität der Antwort), `model` (das zu verwendende KI-Modell).
- **Output:** Der von der KI generierte Antwort-Text als String.
##### `summarize_website_content(raw_text, company_name)`
- **Zweck:** Nutzt die KI, um den Rohtext einer Website zu analysieren und eine strukturierte Zusammenfassung des Geschäftsmodells und des Potenzials für Field Service Management (FSM) zu erstellen.
- **Input:** `raw_text` (der Inhalt der Website), `company_name` (Name des Unternehmens).
- **Output:** Ein formatierter String, der das Geschäftsmodell, das FSM-Potenzial und Belegsätze enthält.
##### `evaluate_branche_chatgpt(...)`
- **Zweck:** Führt eine KI-basierte Brancheneinstufung für ein einzelnes Unternehmen durch. Sendet ein Unternehmensprofil und ein vordefiniertes Branchenschema an die KI.
- **Input:** `company_name`, `website_summary`, `wiki_absatz`.
- **Output:** Ein Dictionary mit der vorgeschlagenen "Branche", "Konfidenz" und "Begruendung".
##### `evaluate_branches_batch(companies_data)`
- **Zweck:** Führt die Brancheneinstufung für eine Liste von Unternehmen in einem einzigen API-Aufruf durch, um die Effizienz zu steigern.
- **Input:** `companies_data` (eine Liste von Dictionaries, die Unternehmensprofile enthalten).
- **Output:** Eine Liste von Dictionaries mit den Ergebnissen für jedes Unternehmen.
##### `generate_fsm_pitch(...)`
- **Zweck:** Generiert einen hochpersonalisierten, einleitenden Satz für eine Marketing-E-Mail (FSM-Pitch), basierend auf den gesammelten Unternehmensdaten.
- **Input:** Diverse Unternehmensdaten wie Name, Branche, Zusammenfassungen und Mitarbeiter-/Technikerzahlen.
- **Output:** Ein einzelner, prägnanter Satz als String.
##### `serp_website_lookup(company_name)`
- **Zweck:** Verwendet die SerpAPI (Google-Suche), um die offizielle Website eines Unternehmens zu finden. Filtert dabei unzuverlässige Quellen wie soziale Medien oder Nachrichtenportale heraus.
- **Input:** `company_name`.
- **Output:** Die normalisierte URL der gefundenen Website oder "k.A.".
##### `search_linkedin_contacts(...)`
- **Zweck:** Führt eine gezielte Google-Suche über die SerpAPI durch, um LinkedIn-Profile von Mitarbeitern in bestimmten Positionen (z.B. "Serviceleiter") bei einem Unternehmen zu finden.
- **Input:** `company_name`, `website`, `position_query` (z.B. "Leiter Kundendienst"), `crm_kurzform`.
- **Output:** Eine Liste von Dictionaries, die die gefundenen Kontakte mit Namen, Position und LinkedIn-URL enthalten.
#### Website Scraping & Validierung
##### `get_website_raw(url, ...)`
- **Zweck:** Lädt den reinen Textinhalt von einer Webseite. Die Funktion ist gehärtet, um mit verschiedenen Fehlern (SSL, Timeout, Connection Errors) umzugehen und versucht, Cookie-Banner intelligent zu entfernen.
- **Input:** `url` (die zu scrapende URL).
- **Output:** Der extrahierte Text der Website als String oder ein Fehlerhinweis (z.B. "k.A. (Timeout)").
##### `is_valid_wikipedia_article_url(url)`
- **Zweck:** Überprüft, ob eine gegebene URL tatsächlich auf einen existierenden Wikipedia-Artikel verweist und nicht auf eine "Seite existiert nicht"-Seite.
- **Input:** `url` (die zu prüfende Wikipedia-URL).
- **Output:** `True`, wenn der Artikel existiert, andernfalls `False`.
---
## 9. Konsolidierte Architektur & Web-Anwendungen
Um die verschiedenen Werkzeuge zugänglicher und wartbarer zu machen, wurden der **B2B Marketing Assistant** und das **Market Intelligence Tool** in einer einzigen, Docker-basierten Architektur konsolidiert. Diese neue Struktur bietet einen zentralen, passwortgeschützten Einstiegspunkt und optimiert sowohl die Entwicklung als auch den Betrieb.
### Architektur im Überblick
Die konsolidierte Lösung besteht aus fünf Haupt-Containern, die von `docker-compose.yml` gesteuert werden:
1. **Proxy (Nginx):**
* **Zentraler Einstiegspunkt:** Der Proxy lauscht auf Port `8090` und ist der einzige von außen erreichbare Punkt.
* **Passwortschutz:** Sichert den gesamten Zugriff mit HTTP Basic Authentication (Benutzer: `admin`, Passwort: `gemini`).
* **Routing:** Leitet Anfragen an die richtigen Anwendungs-Container weiter:
* `/` -> Dashboard (Landingpage)
* `/b2b/` -> B2B Marketing Assistant
* `/market/` -> Market Intelligence Tool
* `/gtm/` -> GTM Architect
2. **Dashboard:**
* Eine simple Nginx-Instanz, die eine statische HTML-Seite (`dashboard/index.html`) ausliefert.
* Diese Seite dient als Landingpage und verlinkt auf die drei untergeordneten Anwendungen.
3. **B2B Marketing Assistant (`b2b-app`):**
* Ein Docker-Container, der das React-Frontend und die dazugehörige Node.js/Python-Backend-Logik enthält.
* Erreichbar über das Unterverzeichnis `/b2b/`.
4. **Market Intelligence (`market-frontend` & `market-backend`):**
* Diese Anwendung bleibt in Frontend- und Backend-Container aufgeteilt, wird aber nun ebenfalls über den zentralen Proxy unter `/market/` geroutet.
5. **GTM Architect (`gtm-app`):**
* Ein Docker-Container, der das React-Frontend und die dazugehörige Node.js/Python-Backend-Logik kapselt.
* Erreichbar über das Unterverzeichnis `/gtm/`.
### Vorteile dieser Architektur
- **Zentraler & Sicherer Zugriff:** Nur ein Port muss freigegeben werden, und der Zugang ist einheitlich geschützt.
- **Einfache Skalierbarkeit:** Die einzelnen Komponenten sind voneinander entkoppelt und können unabhängig voneinander aktualisiert werden.
- **Optimierte Builds:** Durch die Verwendung von `.dockerignore` und Python-Basis-Images werden die Build-Zeiten verkürzt und die Image-Größen reduziert.
- **Effiziente Entwicklung:** Die Python-Skripte (`*.py`) werden als Volumes eingebunden ("Sideloading"). Änderungen am Python-Code erfordern daher **keinen** kompletten Docker-Rebuild, sondern nur einen Neustart des Containers.
---
## 10. Deployment & Entwicklung
### Produktions-Deployment
Für den dauerhaften Betrieb auf einem Server (z.B. Synology Diskstation) wird die gesamte Anwendung mit einem einzigen Befehl gestartet.
1. **Stellen Sie sicher, dass Docker und Docker Compose installiert sind.**
2. **Navigieren Sie in das Hauptverzeichnis des Projekts.**
3. **Starten Sie den gesamten Stack:**
```bash
docker-compose up -d --build
```
Der `--build`-Parameter ist nur beim ersten Start oder nach Änderungen an den Dockerfiles oder Frontend-Komponenten (z.B. `App.tsx`) notwendig.
### Zugriff
- **Marketing & Intelligence Hub:** Die zentrale Landingpage ist unter `http://<Server-IP>:8090` erreichbar.
- **B2B Marketing Assistant:** Direkt erreichbar unter `http://<Server-IP>:8090/b2b/`.
- **Market Intelligence Tool:** Direkt erreichbar unter `http://<Server-IP>:8090/market/`.
- **GTM Architect:** Direkt erreichbar unter `http://<Server-IP>:8090/gtm/`.
### Entwicklung & Sideloading
Dank des Volume-Mountings für die Python-Skripte ist die Entwicklung sehr effizient:
1. **Ändern Sie den Python-Code** in `b2b_marketing_orchestrator.py`, `market_intel_orchestrator.py` oder `gtm_architect_orchestrator.py`.
2. **Starten Sie den betroffenen Container neu**, damit die Änderungen wirksam werden:
```bash
# Für den B2B Assistant
docker-compose restart b2b-app
# Für das Market Intelligence Backend
docker-compose restart market-backend
# Für den GTM Architect
docker-compose restart gtm-app
```
Ein zeitaufwändiger `docker-compose build` ist nur bei Änderungen am Frontend-Code, an den `Dockerfile`n oder an den `requirements.txt`/`package.json`-Dateien notwendig.
### API-Authentifizierung (Company Explorer)
Der `company-explorer` Dienst erfordert nun eine HTTP Basic Authentication für alle API-Endpunkte (`/api/*`). Dies dient dem internen Zugriffsschutz.
Um den Dienst zu nutzen, müssen folgende Umgebungsvariablen in der `docker-compose.yml` (oder in einer `.env`-Datei, wenn diese konfiguriert ist) gesetzt werden:
```yaml
services:
company-explorer:
# ... andere Konfigurationen ...
environment:
API_USER: "IhrBenutzername" # Legen Sie hier Ihren gewünschten Benutzernamen fest
API_PASSWORD: "IhrSicheresPasswort" # Legen Sie hier Ihr gewünschtes sicheres Passwort fest
```
Stellen Sie sicher, dass Sie diese Werte vor dem Start des `company-explorer` Dienstes festlegen. Nach dem Aktualisieren der `docker-compose.yml` müssen Sie den Dienst neu starten:
The application is located in the `heatmap-tool` directory. Please refer to the `README.md` file within that directory for detailed instructions on how to run and use the application.
```bash
docker-compose restart company-explorer
cd heatmap-tool
```
---
## 11. Market Intelligence: Funktionsweise v2.2 (Update Dez. 2025)
Das "Market Intelligence Tool" wurde in Version 2.2 massiv überarbeitet, um robuster und präziser zu arbeiten.
### Neue Features
1. **Robuste Audit-Strategie (Graceful Fallback):**
* Bisher führten blockierte Webseiten (403 Forbidden, Timeouts) zum Abbruch der Analyse.
* **Neu:** Wenn der Scraper blockiert wird, schaltet das System automatisch in einen "Digital Footprint Mode". Es analysiert dann Snippets aus der Google-Suche (Tech-Stack-Spuren, Job-Postings, News), um trotzdem eine Bewertung abzugeben. Der Status wird transparent als "Website Access Denied - Relying on External Signals" gekennzeichnet.
2. **Präzisere Lookalike-Suche:**
* Die KI wurde neu instruiert, strikt zwischen dem "Jäger" (Context/Hochgeladener Report) und der "Beute" (Referenzkunde/URL) zu unterscheiden.
* Die Suche nach "Lookalikes" basiert nun explizit auf dem Geschäftszweck des Referenzkunden, nicht auf dem Angebot des Nutzers.
3. **Erweiterter Report:**
* Der finale Report enthält nun drei neue, strategische Sektionen:
* **Search Strategy ICP:** Detaillierte Beschreibung des idealen Kundenprofils basierend auf der Analyse.
* **Digital Signals:** Konkrete digitale Indikatoren (z.B. "Nutzt MS Teams", "Sucht Projektleiter"), auf die geachtet wurde.
* **Target Pages:** Eine Liste der wichtigsten Unterseiten auf der Ziel-Website (z.B. Karriere, Über uns) für die Recherche.
### Technische Verbesserungen
* **URL Auto-Fix:** URLs ohne `https://` werden automatisch korrigiert.
* **Modern User-Agent:** Der Scraper gibt sich als moderner Chrome-Browser aus, um Blockaden zu minimieren.
* **Frontend-Integration:** Die neuen Datenfelder werden nahtlos im React-Frontend und im Markdown-Export angezeigt.
---
## 12. Infrastructure & Operations
### DNS & Connectivity
Um eine stabile Erreichbarkeit der Dienste zu gewährleisten, wurde eine Docker-basierte DynDNS-Lösung implementiert.
* **[DuckDNS & Monitoring Setup](duckdns_setup.md):** Dokumentation zur Einrichtung des `duckdns` Updaters und des `dns-monitor` Sidecars, um Verbindungsprobleme und Caching-Fehler zu beheben.
---
## 13. Funktionsweise im Detail
### Analyse-Tiefe der "Digital Signals" im Market Intelligence Tool
Die Identifizierung von digitalen Signalen bei Zielunternehmen erfolgt über einen pragmatischen, zweistufigen Prozess, um ein Gleichgewicht zwischen analytischer Tiefe und Performance zu gewährleisten:
1. **Vollständiges Parsen der Unternehmens-Homepage:**
* Die Haupt-URL des zu analysierenden Unternehmens wird einmalig vollständig gecrawlt. Der extrahierte Text (`homepage_text`) dient dem Sprachmodell als grundlegender Kontext, um das Geschäftsmodell und die Kernaussagen des Unternehmens zu verstehen.
2. **Analyse von Suchergebnis-Snippets für Signale:**
* Für die gezielte Suche nach spezifischen Signalen (z.B. eingesetzte Konkurrenzprodukte, offene Stellen, strategische Initiativen) wird die SerpAPI (Google-Suche) genutzt.
* **Wichtig:** Die in den Suchergebnissen gefundenen Ziel-URLs werden **nicht** erneut besucht und geparst. Stattdessen werden ausschließlich der **Titel (`title`)** und der von der Suchmaschine generierte **Textschnipsel (`snippet`)** als "Beweismittel" (`evidence`) an das Sprachmodell übergeben.
* Dieser Ansatz ist ein bewusster Kompromiss: Er ist extrem schnell und kosteneffizient, da er aufwändiges Crawling vermeidet. Die Snippets sind in der Regel aussagekräftig genug, um das Vorhandensein eines Signals mit hoher Wahrscheinlichkeit zu validieren.
### Zukünftige Erweiterung: Detaillierte Analyse von Stellenausschreibungen
Als eine zukünftige, sehr wertvolle Erweiterung ist die detaillierte, automatisierte Analyse von Stellenausschreibungen vorgemerkt.
* **Strategischer Mehrwert:**
* **Einblick in die Wirtschaftslage:** Die Art und Anzahl der offenen Stellen (z.B. Vertrieb vs. Entwicklung vs. Verwaltung) kann Aufschluss über die aktuelle Wachstums-, Konsolidierungs- oder Krisenphase eines Unternehmens geben.
* **IT-Landkarte & Tech-Stack:** Insbesondere IT-Stellenanzeigen sind eine Goldgrube für Technographic-Daten. Sie listen oft explizit die eingesetzten Programmiersprachen, Frameworks, Datenbanken, ERP-Systeme (z.B. SAP, D365) und Cloud-Anbieter auf. Dies erlaubt eine einzigartig detaillierte Erstellung der "IT-Landkarte" eines Zielunternehmens.
* **Herausforderung:**
* Der technische Aufwand für ein robustes System, das Karriereseiten findet, die verschiedenen Job-Portale parst und die relevanten Informationen extrahiert, ist immens.
* **Status:**
* Diese Erweiterung wird für eine spätere Entwicklungsphase vorgemerkt und sollte aufgrund der Komplexität in einem klar abgegrenzten, überschaubaren Rahmen umgesetzt werden.
Inside, you will find a self-contained project with its own `docker-compose.yml` for easy setup.