[30388f42] Infrastructure Hardening: Repaired CE/Connector DB schema, fixed frontend styling build, implemented robust echo shield in worker v2.1.1, and integrated Lead Engine into gateway.

This commit is contained in:
2026-03-07 14:08:42 +00:00
parent 35c30bc39a
commit d1b77fd2f6
415 changed files with 24100 additions and 13301 deletions

View File

@@ -0,0 +1,44 @@
import uuid
import os
import sys
# This is the crucial part to fix the import error.
# We add the 'company-explorer' directory to the path, so imports can be absolute
# from the 'backend' module.
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from backend.database import Contact, SessionLocal
def migrate_existing_contacts():
"""
Generates and adds an unsubscribe_token for all existing contacts
that do not have one yet.
"""
db = SessionLocal()
try:
contacts_to_update = db.query(Contact).filter(Contact.unsubscribe_token == None).all()
if not contacts_to_update:
print("All contacts already have an unsubscribe token. No migration needed.")
return
print(f"Found {len(contacts_to_update)} contacts without an unsubscribe token. Generating tokens...")
for contact in contacts_to_update:
token = str(uuid.uuid4())
contact.unsubscribe_token = token
print(f" - Generated token for contact ID {contact.id} ({contact.email})")
db.commit()
print("\nSuccessfully updated all contacts with new unsubscribe tokens.")
except Exception as e:
print(f"An error occurred: {e}")
db.rollback()
finally:
db.close()
if __name__ == "__main__":
print("Starting migration: Populating unsubscribe_token for existing contacts.")
migrate_existing_contacts()
print("Migration finished.")

View File

@@ -0,0 +1,82 @@
import sys
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from collections import Counter
import re
# Add backend to path to import models
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
from backend.config import settings
from backend.database import Contact, JobRolePattern
def clean_text(text):
if not text: return ""
# Keep only alphanumeric and spaces
text = re.sub(r'[^\w\s]', ' ', text)
return text.lower().strip()
def get_ngrams(tokens, n):
if len(tokens) < n:
return []
return [" ".join(tokens[i:i+n]) for i in range(len(tokens)-n+1)]
def analyze_patterns():
print(f"Connecting to database: {settings.DATABASE_URL}")
engine = create_engine(settings.DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
try:
# Fetch all contacts with a role
contacts = session.query(Contact).filter(Contact.role != None, Contact.job_title != None).all()
print(f"Found {len(contacts)} classified contacts to analyze.")
role_groups = {}
for c in contacts:
if c.role not in role_groups:
role_groups[c.role] = []
role_groups[c.role].append(c.job_title)
print("\n" + "="*60)
print(" JOB TITLE PATTERN ANALYSIS REPORT")
print("="*60 + "\n")
for role, titles in role_groups.items():
print(f"--- ROLE: {role} ({len(titles)} samples) ---")
# Tokenize all titles
all_tokens = []
all_bigrams = []
for t in titles:
cleaned = clean_text(t)
tokens = [w for w in cleaned.split() if len(w) > 2] # Ignore short words
all_tokens.extend(tokens)
all_bigrams.extend(get_ngrams(tokens, 2))
# Analyze frequencies
common_words = Counter(all_tokens).most_common(15)
common_bigrams = Counter(all_bigrams).most_common(10)
print("Top Keywords:")
for word, count in common_words:
print(f" - {word}: {count}")
print("\nTop Bigrams (Word Pairs):")
for bg, count in common_bigrams:
print(f" - \"{bg}\": {count}")
print("\nSuggested Regex Components:")
top_5_words = [w[0] for w in common_words[:5]]
print(f" ({ '|'.join(top_5_words) })")
print("\n" + "-"*30 + "\n")
except Exception as e:
print(f"Error: {e}")
finally:
session.close()
if __name__ == "__main__":
analyze_patterns()

View File

@@ -0,0 +1,22 @@
import sys
import os
# Setup Environment
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
from backend.database import SessionLocal, JobRolePattern
def check_mappings():
db = SessionLocal()
count = db.query(JobRolePattern).count()
print(f"Total JobRolePatterns: {count}")
examples = db.query(JobRolePattern).limit(5).all()
for ex in examples:
print(f" - {ex.pattern} -> {ex.role}")
db.close()
if __name__ == "__main__":
check_mappings()

View File

@@ -0,0 +1,171 @@
import sys
import os
import argparse
import json
import logging
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from datetime import datetime
# --- Standalone Configuration ---
# Add the project root to the Python path to find the LLM utility
sys.path.insert(0, '/app')
from company_explorer.backend.lib.core_utils import call_gemini_flash
DATABASE_URL = "sqlite:////app/companies_v3_fixed_2.db"
LOG_FILE = "/app/Log_from_docker/batch_classifier.log"
BATCH_SIZE = 50 # Number of titles to process in one LLM call
# --- Logging Setup ---
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# --- SQLAlchemy Models (self-contained) ---
Base = declarative_base()
class RawJobTitle(Base):
__tablename__ = 'raw_job_titles'
id = Column(Integer, primary_key=True)
title = Column(String, unique=True, index=True)
count = Column(Integer, default=1)
source = Column(String)
is_mapped = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class JobRolePattern(Base):
__tablename__ = "job_role_patterns"
id = Column(Integer, primary_key=True, index=True)
pattern_type = Column(String, default="exact", index=True)
pattern_value = Column(String, unique=True)
role = Column(String, index=True)
priority = Column(Integer, default=100)
is_active = Column(Boolean, default=True)
created_by = Column(String, default="system")
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Persona(Base):
__tablename__ = "personas"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
pains = Column(String)
gains = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# --- Database Connection ---
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def build_classification_prompt(titles_to_classify, available_roles):
"""Builds the prompt for the LLM to classify a batch of job titles."""
prompt = f"""
You are an expert in B2B contact segmentation. Your task is to classify a list of job titles into predefined roles.
Analyze the following list of job titles and assign each one to the most appropriate role from the list provided.
The available roles are:
- {', '.join(available_roles)}
RULES:
1. Respond ONLY with a valid JSON object. Do not include any text, explanations, or markdown code fences before or after the JSON.
2. The JSON object should have the original job title as the key and the assigned role as the value.
3. If a job title is ambiguous or you cannot confidently classify it, assign the value "Influencer". Use this as a fallback.
4. Do not invent new roles. Only use the roles from the provided list.
Here are the job titles to classify:
{json.dumps(titles_to_classify, indent=2)}
Your JSON response:
"""
return prompt
def classify_and_store_titles():
db = SessionLocal()
try:
# 1. Fetch available persona names (roles)
personas = db.query(Persona).all()
available_roles = [p.name for p in personas]
if not available_roles:
logger.error("No Personas/Roles found in the database. Cannot classify. Please seed personas first.")
return
logger.info(f"Classifying based on these roles: {available_roles}")
# 2. Fetch unmapped titles
unmapped_titles = db.query(RawJobTitle).filter(RawJobTitle.is_mapped == False).all()
if not unmapped_titles:
logger.info("No unmapped job titles found. Nothing to do.")
return
logger.info(f"Found {len(unmapped_titles)} unmapped job titles to process.")
# 3. Process in batches
for i in range(0, len(unmapped_titles), BATCH_SIZE):
batch = unmapped_titles[i:i + BATCH_SIZE]
title_strings = [item.title for item in batch]
logger.info(f"Processing batch {i//BATCH_SIZE + 1} of { (len(unmapped_titles) + BATCH_SIZE - 1) // BATCH_SIZE } with {len(title_strings)} titles...")
# 4. Call LLM
prompt = build_classification_prompt(title_strings, available_roles)
response_text = ""
try:
response_text = call_gemini_flash(prompt, json_mode=True)
# Clean potential markdown fences
if response_text.strip().startswith("```json"):
response_text = response_text.strip()[7:-4]
classifications = json.loads(response_text)
except Exception as e:
logger.error(f"Failed to get or parse LLM response for batch. Skipping. Error: {e}")
logger.error(f"Raw response was: {response_text}")
continue
# 5. Process results
new_patterns = 0
for title_obj in batch:
original_title = title_obj.title
assigned_role = classifications.get(original_title)
if assigned_role and assigned_role in available_roles:
exists = db.query(JobRolePattern).filter(JobRolePattern.pattern_value == original_title).first()
if not exists:
new_pattern = JobRolePattern(
pattern_type='exact',
pattern_value=original_title,
role=assigned_role,
priority=90,
created_by='llm_batch'
)
db.add(new_pattern)
new_patterns += 1
title_obj.is_mapped = True
else:
logger.warning(f"Could not classify '{original_title}' or role '{assigned_role}' is invalid. It will be re-processed later.")
db.commit()
logger.info(f"Batch {i//BATCH_SIZE + 1} complete. Created {new_patterns} new mapping patterns.")
except Exception as e:
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
db.rollback()
finally:
db.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Batch classify unmapped job titles using an LLM.")
args = parser.parse_args()
logger.info("--- Starting Batch Classification Script ---")
classify_and_store_titles()
logger.info("--- Batch Classification Script Finished ---")

View File

@@ -0,0 +1,64 @@
import sys
import os
import argparse
# Adjust the path to include the 'company-explorer' directory
# This allows the script to find the 'backend' module
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from backend.database import SessionLocal, Industry, Persona, MarketingMatrix
def check_texts_for_industry(industry_name: str):
"""
Fetches and prints the marketing matrix texts for all personas
within a specific industry.
"""
db = SessionLocal()
try:
industry = db.query(Industry).filter(Industry.name == industry_name).first()
if not industry:
print(f"Error: Industry '{industry_name}' not found.")
# Suggest similar names
all_industries = db.query(Industry.name).all()
print("\nAvailable Industries:")
for (name,) in all_industries:
print(f"- {name}")
return
entries = (
db.query(MarketingMatrix)
.join(Persona)
.filter(MarketingMatrix.industry_id == industry.id)
.order_by(Persona.id)
.all()
)
if not entries:
print(f"No marketing texts found for industry: {industry_name}")
return
print(f"--- NEW TEXTS FOR {industry_name} ---")
for entry in entries:
print(f"\nPERSONA: {entry.persona.name}")
print(f"Subject: {entry.subject}")
print(f"Intro: {entry.intro.replace(chr(10), ' ')}") # Replace newlines for cleaner one-line output
except Exception as e:
print(f"An error occurred: {e}")
finally:
db.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Debug and check marketing matrix texts for a given industry.")
parser.add_argument(
"industry",
type=str,
nargs='?',
default="Healthcare - Hospital",
help="The name of the industry to check (e.g., 'Healthcare - Hospital'). Defaults to 'Healthcare - Hospital'."
)
args = parser.parse_args()
check_texts_for_industry(args.industry)

View File

@@ -0,0 +1,94 @@
import os
import sys
import argparse
import logging
from typing import Any # Hinzugefügt
# Add the company-explorer directory to the Python path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from backend.database import get_db, Company, Industry, Persona # Added Industry and Persona for full context
from backend.services.classification import ClassificationService
from backend.lib.logging_setup import setup_logging
# --- CONFIGURATION ---
# Setup logging to be very verbose for this script
setup_logging()
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def run_debug_analysis(company_identifier: Any, is_id: bool):
"""
Runs the full classification and enrichment process for a single company
in the foreground and prints detailed results.
"""
logger.info(f"--- Starting Interactive Debug for Company: {company_identifier} (by {'ID' if is_id else 'Name'}) ---")
db_session = next(get_db())
try:
# 1. Fetch the company
if is_id:
company = db_session.query(Company).filter(Company.id == company_identifier).first()
else:
company = db_session.query(Company).filter(Company.name == company_identifier).first()
if not company:
logger.error(f"Company with {'ID' if is_id else 'Name'} {company_identifier} not found.")
# If by name, suggest similar names
if not is_id:
all_company_names = db_session.query(Company.name).limit(20).all()
print("\nAvailable Company Names (first 20):")
for (name,) in all_company_names:
print(f"- {name}")
return
logger.info(f"Found Company: {company.name} (ID: {company.id})")
# --- PRE-ANALYSIS STATE ---
print("\n--- METRICS BEFORE ---")
print(f"Calculated: {company.calculated_metric_value} {company.calculated_metric_unit}")
print(f"Standardized: {company.standardized_metric_value} {company.standardized_metric_unit}")
print(f"Opener 1 (Infra): {company.ai_opener}")
print(f"Opener 2 (Ops): {company.ai_opener_secondary}")
print("----------------------\n")
# 2. Instantiate the service
classifier = ClassificationService()
# 3. RUN THE CORE LOGIC
# This will now print all the detailed logs we added
updated_company = classifier.classify_company_potential(company, db_session)
# --- POST-ANALYSIS STATE ---
print("\n--- METRICS AFTER ---")
print(f"Industry (AI): {updated_company.industry_ai}")
print(f"Metric Source: {updated_company.metric_source}")
print(f"Proof Text: {updated_company.metric_proof_text}")
print(f"Calculated: {updated_company.calculated_metric_value} {updated_company.calculated_metric_unit}")
print(f"Standardized: {updated_company.standardized_metric_value} {updated_company.standardized_metric_unit}")
print(f"\nOpener 1 (Infra): {updated_company.ai_opener}")
print(f"Opener 2 (Ops): {updated_company.ai_opener_secondary}")
print("---------------------")
logger.info(f"--- Interactive Debug Finished for Company: {company.name} (ID: {company.id}) ---")
except Exception as e:
logger.error(f"An error occurred during analysis: {e}", exc_info=True)
finally:
db_session.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run a single company analysis for debugging.")
parser.add_argument("--id", type=int, help="The ID of the company to analyze.")
parser.add_argument("--company-name", type=str, help="The name of the company to analyze.")
args = parser.parse_args()
if args.id and args.company_name:
parser.error("Please provide either --id or --company-name, not both.")
elif args.id:
run_debug_analysis(args.id, is_id=True)
elif args.company_name:
run_debug_analysis(args.company_name, is_id=False)
else:
parser.error("Please provide either --id or --company-name.")

View File

@@ -0,0 +1,307 @@
import sys
import os
import json
import argparse
import re
import google.generativeai as genai
# Setup Environment
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
from backend.database import SessionLocal, Industry, Persona, MarketingMatrix
from backend.config import settings
# --- Configuration ---
MODEL_NAME = "gemini-2.0-flash" # High quality copy
def extract_segment(text: str, marker: str) -> str:
"""
Extracts a text block starting with [marker].
Example: [Primary Product: Cleaning] ... [Secondary Product: Service]
"""
if not text: return ""
# Split by square brackets that look like headers [Text: ...]
# We look for the marker inside the header
# Simplified Regex: Capture everything inside brackets as ONE group
pattern = r'\[(.*?)\]'
segments = re.split(pattern, text)
# segments[0] is text before first bracket
# segments[1] is content of first bracket (header)
# segments[2] is content after first bracket (body)
# ...
best_match = ""
for i in range(1, len(segments), 2):
header = segments[i]
content = segments[i+1]
# print(f"DEBUG: Checking Header: '{header}' for Marker: '{marker}'") # Uncomment for deep debug
if marker.lower() in header.lower():
return content.strip()
# Fallback: If no markers found, return full text (legacy support)
if "Primary Product" not in text and "Secondary Product" not in text:
return text
return ""
def generate_prompt(industry: Industry, persona: Persona) -> str:
"""
Builds the prompt for the AI to generate the marketing texts.
Combines Industry context with Persona specific pains/gains and Product Category.
"""
# 1. Determine Product Focus Strategy
# Default: Primary
target_scope = "Primary Product"
target_category = industry.primary_category
# Special Rule: "Operativer Entscheider" gets Secondary Product IF ops_focus_secondary is True
# Logic: A Nursing Director (Ops) doesn't care about floor cleaning (Facility),
# but cares about Service Robots (Secondary).
if persona.name == "Operativer Entscheider" and industry.ops_focus_secondary:
target_scope = "Secondary Product"
target_category = industry.secondary_category
print(f" -> STRATEGY SWITCH: Using {target_scope} for {persona.name}")
# Fallback if secondary was requested but not defined
if not target_category:
target_category = industry.primary_category
target_scope = "Primary Product" # Fallback to primary if secondary category object is missing
product_context = f"{target_category.name}: {target_category.description}" if target_category else "Intelligente Robotik-Lösungen"
# 2. Extract specific segments from industry pains/gains based on scope
industry_pains = extract_segment(industry.pains, target_scope)
industry_gains = extract_segment(industry.gains, target_scope)
# Fallback: If specific scope is empty (e.g. no Secondary Pains defined), try Primary
if not industry_pains and target_scope == "Secondary Product":
print(f" -> WARNING: No specific Pains found for {target_scope}. Fallback to Primary.")
industry_pains = extract_segment(industry.pains, "Primary Product")
industry_gains = extract_segment(industry.gains, "Primary Product")
# 3. Handle Persona Data
try:
persona_pains = json.loads(persona.pains) if persona.pains else []
persona_gains = json.loads(persona.gains) if persona.gains else []
except:
persona_pains = [persona.pains] if persona.pains else []
persona_gains = [persona.gains] if persona.gains else []
# Advanced Persona Context
persona_context = f"""
BESCHREIBUNG/DENKWEISE: {persona.description or 'Nicht definiert'}
WAS DIESE PERSON ÜBERZEUGT: {persona.convincing_arguments or 'Nicht definiert'}
RELEVANTE KPIs: {persona.kpis or 'Nicht definiert'}
"""
prompt = f"""
Du bist ein kompetenter Lösungsberater und brillanter Texter für B2B-Marketing.
AUFGABE: Erstelle 3 hoch-personalisierte Textblöcke (Subject, Introduction_Textonly, Industry_References_Textonly) für eine E-Mail an einen Entscheider.
--- KONTEXT ---
ZIELBRANCHE: {industry.name}
BRANCHEN-HERAUSFORDERUNGEN (PAIN POINTS):
{industry_pains}
FOKUS-PRODUKT (LÖSUNG):
{product_context}
ANSPRECHPARTNER (ROLLE): {persona.name}
{persona_context}
SPEZIFISCHE HERAUSFORDERUNGEN (PAIN POINTS) DER ROLLE:
{chr(10).join(['- ' + str(p) for p in persona_pains])}
SPEZIFISCHE NUTZEN (GAINS) DER ROLLE:
{chr(10).join(['- ' + str(g) for g in persona_gains])}
HINTERGRUNDWISSEN & STRATEGIE (Miller Heiman):
{industry.strategy_briefing or 'Kein spezifisches Briefing verfügbar.'}
--- DEINE AUFGABE ---
Deine Texte müssen "voll ins Zentrum" der Rolle treffen. Vermeide oberflächliche Floskeln. Nutze die Details zur Denkweise, den KPIs und den Überzeugungsargumenten, um eine tiefgreifende Relevanz zu erzeugen.
Nutze das Strategie-Briefing, um typische Einwände vorwegzunehmen oder "Red Flags" zu vermeiden.
1. **Subject:** Formuliere eine kurze Betreffzeile (max. 6 Wörter). Richte sie **direkt an einem der persönlichen Pain Points** des Ansprechpartners oder dem zentralen Branchen-Pain. Sei scharfsinnig, nicht werblich.
2. **Introduction_Textonly:** Formuliere einen prägnanten Einleitungstext (max. 2 Sätze).
- **WICHTIG:** Gehe davon aus, dass die spezifische Herausforderung des Kunden bereits im Satz davor [Opener] genannt wurde. **Wiederhole die Herausforderung NICHT.**
- **Satz 1 (Die Lösung & der Gain):** Beginne direkt mit der Lösung. Nenne die im Kontext `FOKUS-PRODUKT` definierte **Produktkategorie** (z.B. "automatisierte Reinigungsroboter") und verbinde sie mit einem Nutzen, der für diese Rolle (siehe `WAS DIESE PERSON ÜBERZEUGT` und `GAINS`) besonders kritisch ist.
- **Satz 2 (Die Relevanz):** Stelle die Relevanz für die Zielperson her, indem du eine ihrer `PERSÖNLICHE HERAUSFORDERUNGEN` oder `KPIs` adressierst. Beispiel: "Für Sie als [Rolle] bedeutet dies vor allem [Nutzen bezogen auf KPI oder Pain]."
3. **Industry_References_Textonly:** Formuliere einen **strategischen Referenz-Block (ca. 2-3 Sätze)** nach folgendem Muster:
- **Satz 1 (Social Proof):** Beginne direkt mit dem Nutzen, den vergleichbare Unternehmen in der Branche {industry.name} bereits erzielen. (Erfinde keine Firmennamen, sprich von "Führenden Einrichtungen" oder "Vergleichbaren Häusern").
- **Satz 2 (Rollen-Relevanz):** Schaffe den direkten Nutzen für die Zielperson. Nutze dabei die Informationen aus `BESCHREIBUNG/DENKWEISE`, um den Ton perfekt zu treffen.
--- BEISPIEL FÜR EINEN PERFEKTEN OUTPUT ---
{{
"Subject": "Kostenkontrolle im Service",
"Introduction_Textonly": "Genau bei der Optimierung dieser Serviceprozesse können erhebliche Effizienzgewinne erzielt werden. Für Sie als Finanzleiter ist dabei die Sicherstellung der Profitabilität bei gleichzeitiger Kostentransparenz von zentraler Bedeutung.",
"Industry_References_Textonly": "Vergleichbare Unternehmen profitieren bereits massiv von automatisierten Prozessen. Unsere Erfahrung zeigt, dass die grundlegenden Herausforderungen in der Einsatzplanung oft branchenübergreifend ähnlich sind. Dieser Wissensvorsprung hilft uns, Ihre Ziele bei der Kostenkontrolle und Profitabilitätssteigerung besonders effizient zu unterstützen."
}}
--- FORMAT ---
Antworte NUR mit einem validen JSON-Objekt. Keine Markdown-Blöcke (```json), kein erklärender Text.
Format:
{{
"subject": "...",
"intro": "...",
"social_proof": "..."
}}
"""
return prompt
def mock_call(prompt: str):
"""Simulates an API call for dry runs."""
print(f"\n--- [MOCK] GENERATING PROMPT ---\n{prompt[:800]}...\n--------------------------------")
return {
"subject": "[MOCK] Effizienzsteigerung in der Produktion",
"intro": "[MOCK] Als Produktionsleiter wissen Sie, wie teuer Stillstand ist. Unsere Roboter helfen.",
"social_proof": "[MOCK] Ähnliche Betriebe sparten 20% Kosten."
}
def real_gemini_call(prompt: str):
if not settings.GEMINI_API_KEY:
raise ValueError("GEMINI_API_KEY not set in config/env")
genai.configure(api_key=settings.GEMINI_API_KEY)
# Configure Model
generation_config = {
"temperature": 0.7,
"top_p": 0.95,
"top_k": 64,
"max_output_tokens": 1024,
"response_mime_type": "application/json",
}
model = genai.GenerativeModel(
model_name=MODEL_NAME,
generation_config=generation_config,
)
response = model.generate_content(prompt)
try:
# Clean response if necessary (Gemini usually returns clean JSON with mime_type set, but safety first)
text = response.text.strip()
if text.startswith("```json"):
text = text[7:-3].strip()
elif text.startswith("```"):
text = text[3:-3].strip()
parsed_json = json.loads(text)
if isinstance(parsed_json, list):
if len(parsed_json) > 0:
return parsed_json[0]
else:
raise ValueError("Empty list returned from API")
return parsed_json
except Exception as e:
print(f"JSON Parse Error: {e}. Raw Response: {response.text}")
raise
def run_matrix_generation(dry_run: bool = True, force: bool = False, specific_industry: str = None):
db = SessionLocal()
try:
query = db.query(Industry)
if specific_industry:
query = query.filter(Industry.name == specific_industry)
industries = query.all()
personas = db.query(Persona).all()
print(f"Found {len(industries)} Industries and {len(personas)} Personas.")
print(f"Mode: {'DRY RUN (No API calls, no DB writes)' if dry_run else 'LIVE - GEMINI GENERATION'}")
# Pre-load categories to avoid lazy load issues if detached
# (SQLAlchemy session is open, so should be fine, but good practice)
total_combinations = len(industries) * len(personas)
processed = 0
for ind in industries:
print(f"\n>>> Processing Industry: {ind.name} (Ops Secondary: {ind.ops_focus_secondary})")
for pers in personas:
processed += 1
print(f"[{processed}/{total_combinations}] Check: {ind.name} x {pers.name}")
# Check existing
existing = db.query(MarketingMatrix).filter(
MarketingMatrix.industry_id == ind.id,
MarketingMatrix.persona_id == pers.id
).first()
if existing and not force:
print(f" -> Skipped (Already exists)")
continue
# Generate
prompt = generate_prompt(ind, pers)
if dry_run:
result = mock_call(prompt)
else:
try:
result = real_gemini_call(prompt)
# Normalize Keys (Case-Insensitive)
normalized_result = {}
for k, v in result.items():
normalized_result[k.lower()] = v
# Map known variations to standardized keys
if "introduction_textonly" in normalized_result:
normalized_result["intro"] = normalized_result["introduction_textonly"]
if "industry_references_textonly" in normalized_result:
normalized_result["social_proof"] = normalized_result["industry_references_textonly"]
# Validation using normalized keys
if not normalized_result.get("subject") or not normalized_result.get("intro"):
print(f" -> Invalid result structure. Keys found: {list(result.keys())}")
print(f" -> Raw Result: {json.dumps(result, indent=2)}")
continue
except Exception as e:
print(f" -> API ERROR: {e}")
continue
# Write to DB (only if not dry run)
if not dry_run:
if not existing:
new_entry = MarketingMatrix(
industry_id=ind.id,
persona_id=pers.id,
subject=normalized_result.get("subject"),
intro=normalized_result.get("intro"),
social_proof=normalized_result.get("social_proof")
)
db.add(new_entry)
print(f" -> Created new entry.")
else:
existing.subject = normalized_result.get("subject")
existing.intro = normalized_result.get("intro")
existing.social_proof = normalized_result.get("social_proof")
print(f" -> Updated entry.")
db.commit()
except Exception as e:
print(f"Error: {e}")
finally:
db.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--live", action="store_true", help="Actually call Gemini and write to DB")
parser.add_argument("--force", action="store_true", help="Overwrite existing matrix entries")
parser.add_argument("--industry", type=str, help="Specific industry name to process")
args = parser.parse_args()
run_matrix_generation(dry_run=not args.live, force=args.force, specific_industry=args.industry)

View File

@@ -0,0 +1,66 @@
import sys
import os
import csv
from collections import Counter
import argparse
# Add the 'backend' directory to the path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from database import SessionLocal, RawJobTitle
from lib.logging_setup import setup_logging
import logging
setup_logging()
logger = logging.getLogger(__name__)
def import_job_titles_from_csv(file_path: str):
db = SessionLocal()
try:
logger.info(f"Starting import of job titles from {file_path}")
# Use Counter to get frequencies directly from the CSV
job_title_counts = Counter()
total_rows = 0
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
# Assuming the CSV contains only job titles, one per row
for row in reader:
if row and row[0].strip():
title = row[0].strip()
job_title_counts[title] += 1
total_rows += 1
logger.info(f"Read {total_rows} total job title entries. Found {len(job_title_counts)} unique titles.")
added_count = 0
updated_count = 0
for title, count in job_title_counts.items():
existing_title = db.query(RawJobTitle).filter(RawJobTitle.title == title).first()
if existing_title:
if existing_title.count != count:
existing_title.count = count
updated_count += 1
# If it exists and count is the same, do nothing.
else:
new_title = RawJobTitle(title=title, count=count, source="csv_import", is_mapped=False)
db.add(new_title)
added_count += 1
db.commit()
logger.info(f"Import complete. Added {added_count} new unique titles, updated {updated_count} existing titles.")
except Exception as e:
logger.error(f"Error during job title import: {e}", exc_info=True)
db.rollback()
finally:
db.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Import job titles from a CSV file into the RawJobTitle database table.")
parser.add_argument("file_path", type=str, help="Path to the CSV file containing job titles.")
args = parser.parse_args()
import_job_titles_from_csv(args.file_path)

View File

@@ -0,0 +1,58 @@
import sqlite3
import json
DB_PATH = "/app/companies_v3_fixed_2.db"
def inspect(name_part):
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
print(f"Searching for '{name_part}' in {DB_PATH}...")
cursor.execute("SELECT id, name, website, industry_ai, calculated_metric_value, standardized_metric_value, ai_opener, ai_opener_secondary FROM companies WHERE name LIKE ?", (f'%{name_part}%',))
companies = cursor.fetchall()
if not companies:
print("No hits.")
return
for c in companies:
cid, name, website, industry, metric, std_metric, opener_primary, opener_secondary = c
print("\n" + "="*40)
print(f"🏢 {name} (ID: {cid})")
print(f" Vertical: {industry}")
print(f" Website: {website}")
print(f" Metric: {metric} (Std: {std_metric})")
print(f" Opener (Primary): {opener_primary}")
print(f" Opener (Secondary): {opener_secondary}")
# Fetch Enrichment Data
cursor.execute("SELECT source_type, content FROM enrichment_data WHERE company_id = ?", (cid,))
rows = cursor.fetchall()
print("\n 📚 Enrichment Data:")
for r in rows:
stype, content_raw = r
print(f" - {stype}")
try:
content = json.loads(content_raw)
if stype == "website_scrape":
summary = content.get("summary", "")
raw = content.get("text", "")
print(f" > Summary: {summary[:150]}...")
print(f" > Raw Length: {len(raw)}")
if len(raw) > 500:
print(f" > Raw Snippet: {raw[:300]}...")
elif stype == "wikipedia":
print(f" > URL: {content.get('url')}")
intro = content.get("intro_text", "") or content.get("full_text", "")
print(f" > Intro: {str(intro)[:150]}...")
except:
print(" > (Content not valid JSON)")
except Exception as e:
print(f"Error: {e}")
finally:
if conn: conn.close()
if __name__ == "__main__":
inspect("Therme Erding")

View File

@@ -0,0 +1,58 @@
import sys
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Add backend path
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
from backend.database import Company, EnrichmentData
from backend.config import settings
def inspect_company(company_name_part):
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
db = SessionLocal()
try:
print(f"Searching for company containing: '{company_name_part}'...")
companies = db.query(Company).filter(Company.name.ilike(f"%{company_name_part}%")).all()
if not companies:
print("❌ No company found.")
return
for company in companies:
print("\n" + "="*60)
print(f"🏢 COMPANY: {company.name} (ID: {company.id})")
print("="*60)
print(f"🌐 Website: {company.website}")
print(f"🏗️ Industry (AI): {company.industry_ai}")
print(f"📊 Metric: {company.calculated_metric_value} {company.calculated_metric_unit} (Std: {company.standardized_metric_value} m²)")
print(f"✅ Status: {company.status}")
# Enrichment Data
enrichment = db.query(EnrichmentData).filter(EnrichmentData.company_id == company.id).all()
print("\n📚 ENRICHMENT DATA:")
for ed in enrichment:
print(f" 🔹 Type: {ed.source_type} (Locked: {ed.is_locked})")
if ed.source_type == "website_scrape":
content = ed.content
if isinstance(content, dict):
summary = content.get("summary", "No summary")
raw_text = content.get("raw_text", "")
print(f" 📝 Summary: {str(summary)[:200]}...")
print(f" 📄 Raw Text Length: {len(str(raw_text))} chars")
elif ed.source_type == "wikipedia":
content = ed.content
if isinstance(content, dict):
print(f" 🔗 Wiki URL: {content.get('url')}")
print(f" 📄 Content Snippet: {str(content.get('full_text', ''))[:200]}...")
except Exception as e:
print(f"Error: {e}")
finally:
db.close()
if __name__ == "__main__":
inspect_company("Therme Erding")

View File

@@ -89,6 +89,17 @@ def migrate_tables():
""")
logger.info("Table 'reported_mistakes' ensured to exist.")
# 4. Update CONTACTS Table (Two-step for SQLite compatibility)
logger.info("Checking 'contacts' table schema for unsubscribe_token...")
contacts_columns = get_table_columns(cursor, "contacts")
if 'unsubscribe_token' not in contacts_columns:
logger.info("Adding column 'unsubscribe_token' to 'contacts' table...")
cursor.execute("ALTER TABLE contacts ADD COLUMN unsubscribe_token TEXT")
logger.info("Creating UNIQUE index on 'unsubscribe_token' column...")
cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_unsubscribe_token ON contacts (unsubscribe_token)")
conn.commit()
logger.info("All migrations completed successfully.")

View File

@@ -0,0 +1,43 @@
import sys
import os
# Pfade so setzen, dass das Backend gefunden wird
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from backend.database import SessionLocal, engine
from sqlalchemy import text
def migrate():
print("🚀 Starting Migration: Adding 'campaign_tag' to MarketingMatrix...")
conn = engine.connect()
try:
# 1. Prüfen, ob Spalte schon existiert
# SQLite Pragma: table_info(marketing_matrix)
result = conn.execute(text("PRAGMA table_info(marketing_matrix)")).fetchall()
columns = [row[1] for row in result]
if "campaign_tag" in columns:
print("✅ Column 'campaign_tag' already exists. Skipping.")
return
# 2. Spalte hinzufügen (SQLite supports simple ADD COLUMN)
print("Adding column 'campaign_tag' (DEFAULT 'standard')...")
conn.execute(text("ALTER TABLE marketing_matrix ADD COLUMN campaign_tag VARCHAR DEFAULT 'standard'"))
# 3. Index erstellen (Optional, aber gut für Performance)
print("Creating index on 'campaign_tag'...")
conn.execute(text("CREATE INDEX ix_marketing_matrix_campaign_tag ON marketing_matrix (campaign_tag)"))
conn.commit()
print("✅ Migration successful!")
except Exception as e:
print(f"❌ Migration failed: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == "__main__":
migrate()

View File

@@ -0,0 +1,31 @@
from sqlalchemy import create_engine, text
import sys
import os
# Add backend path
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
from backend.config import settings
def migrate():
engine = create_engine(settings.DATABASE_URL)
with engine.connect() as conn:
try:
# Check if column exists
print("Checking schema...")
# SQLite specific pragma
result = conn.execute(text("PRAGMA table_info(companies)"))
columns = [row[1] for row in result.fetchall()]
if "ai_opener" in columns:
print("Column 'ai_opener' already exists. Skipping.")
else:
print("Adding column 'ai_opener' to 'companies' table...")
conn.execute(text("ALTER TABLE companies ADD COLUMN ai_opener TEXT"))
conn.commit()
print("✅ Migration successful.")
except Exception as e:
print(f"❌ Migration failed: {e}")
if __name__ == "__main__":
migrate()

View File

@@ -0,0 +1,112 @@
import os
import requests
import json
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_VERTICALS = "2ec88f4285448014ab38ea664b4c2b81"
NOTION_DB_PRODUCTS = "2ec88f42854480f0b154f7a07342eb58"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
def fetch_all_pages(db_id):
pages = []
has_more = True
start_cursor = None
while has_more:
url = f"https://api.notion.com/v1/databases/{db_id}/query"
payload = {"page_size": 100}
if start_cursor:
payload["start_cursor"] = start_cursor
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200:
print(f"Error fetching DB {db_id}: {response.status_code} - {response.text}")
break
data = response.json()
pages.extend(data.get("results", []))
has_more = data.get("has_more", False)
start_cursor = data.get("next_cursor")
return pages
def get_property_text(page, prop_name):
props = page.get("properties", {})
prop = props.get(prop_name)
if not prop:
return ""
prop_type = prop.get("type")
if prop_type == "title":
return "".join([t["plain_text"] for t in prop.get("title", [])])
elif prop_type == "rich_text":
return "".join([t["plain_text"] for t in prop.get("rich_text", [])])
elif prop_type == "select":
select = prop.get("select")
return select.get("name") if select else ""
elif prop_type == "multi_select":
return ", ".join([s["name"] for s in prop.get("multi_select", [])])
elif prop_type == "relation":
return [r["id"] for r in prop.get("relation", [])]
else:
return f"[Type: {prop_type}]"
def main():
print("--- 1. Fetching Product Categories ---")
product_pages = fetch_all_pages(NOTION_DB_PRODUCTS)
product_map = {}
for p in product_pages:
p_id = p["id"]
# Product Category name is likely the title property
# Let's find the title property key dynamically
title_key = next((k for k, v in p["properties"].items() if v["id"] == "title"), "Name")
name = get_property_text(p, title_key)
product_map[p_id] = name
# print(f"Product: {name} ({p_id})")
print(f"Loaded {len(product_map)} products.")
print("\n--- 2. Fetching Verticals ---")
vertical_pages = fetch_all_pages(NOTION_DB_VERTICALS)
print("\n--- 3. Analysis ---")
for v in vertical_pages:
# Determine Title Key (Vertical Name)
title_key = next((k for k, v in v["properties"].items() if v["id"] == "title"), "Vertical")
vertical_name = get_property_text(v, title_key)
# Primary Product
pp_ids = get_property_text(v, "Primary Product Category")
pp_names = [product_map.get(pid, f"Unknown ({pid})") for pid in pp_ids] if isinstance(pp_ids, list) else []
# Secondary Product
sp_ids = get_property_text(v, "Secondary Product")
sp_names = [product_map.get(pid, f"Unknown ({pid})") for pid in sp_ids] if isinstance(sp_ids, list) else []
# Pains & Gains
pains = get_property_text(v, "Pains")
gains = get_property_text(v, "Gains")
print(f"\n### {vertical_name}")
print(f"**Primary Product:** {', '.join(pp_names)}")
print(f"**Secondary Product:** {', '.join(sp_names)}")
print(f"**Pains:**\n{pains.strip()}")
print(f"**Gains:**\n{gains.strip()}")
print("-" * 40)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,117 @@
import os
import requests
import json
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" # ID from the user's link
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found in environment.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
def get_vertical_data(vertical_name):
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"contains": vertical_name
}
}
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200:
print(f"Error fetching data for '{vertical_name}': {response.status_code} - {response.text}")
return None
results = response.json().get("results", [])
if not results:
print(f"No entry found for vertical '{vertical_name}'")
return None
# Assuming the first result is the correct one
page = results[0]
props = page["properties"]
# Extract Pains
pains_prop = props.get("Pains", {}).get("rich_text", [])
pains = pains_prop[0]["plain_text"] if pains_prop else "N/A"
# Extract Gains
gains_prop = props.get("Gains", {}).get("rich_text", [])
gains = gains_prop[0]["plain_text"] if gains_prop else "N/A"
# Extract Ops Focus (Checkbox) if available
# The property name might be "Ops. Focus: Secondary" based on user description
# Let's check keys to be sure, but user mentioned "Ops. Focus: Secondary"
# Actually, let's just dump the keys if needed, but for now try to guess
ops_focus = "Unknown"
if "Ops. Focus: Secondary" in props:
ops_focus = props["Ops. Focus: Secondary"].get("checkbox", False)
elif "Ops Focus" in props: # Fallback guess
ops_focus = props["Ops Focus"].get("checkbox", False)
# Extract Product Categories
primary_product = "N/A"
secondary_product = "N/A"
# Assuming these are Select or Multi-select fields, or Relations.
# User mentioned "Primary Product Category" and "Secondary Product Category".
if "Primary Product Category" in props:
pp_data = props["Primary Product Category"].get("select") or props["Primary Product Category"].get("multi_select")
if pp_data:
if isinstance(pp_data, list):
primary_product = ", ".join([item["name"] for item in pp_data])
else:
primary_product = pp_data["name"]
if "Secondary Product Category" in props:
sp_data = props["Secondary Product Category"].get("select") or props["Secondary Product Category"].get("multi_select")
if sp_data:
if isinstance(sp_data, list):
secondary_product = ", ".join([item["name"] for item in sp_data])
else:
secondary_product = sp_data["name"]
return {
"name": vertical_name,
"pains": pains,
"gains": gains,
"ops_focus_secondary": ops_focus,
"primary_product": primary_product,
"secondary_product": secondary_product
}
verticals_to_check = [
"Krankenhaus",
"Pflege", # Might be "Altenheim" or similar
"Hotel",
"Industrie", # Might be "Manufacturing"
"Logistik",
"Einzelhandel",
"Facility Management"
]
print("-" * 60)
for v in verticals_to_check:
data = get_vertical_data(v)
if data:
print(f"VERTICAL: {data['name']}")
print(f" Primary Product: {data['primary_product']}")
print(f" Secondary Product: {data['secondary_product']}")
print(f" Ops. Focus Secondary: {data['ops_focus_secondary']}")
print(f" PAINS: {data['pains']}")
print(f" GAINS: {data['gains']}")
print("-" * 60)

View File

@@ -0,0 +1,90 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" # Verticals DB
PRODUCT_DB_ID = "2ec88f42854480f0b154f7a07342eb58" # Product Categories DB (from user link)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
# 1. Fetch Product Map (ID -> Name)
product_map = {}
def fetch_products():
url = f"https://api.notion.com/v1/databases/{PRODUCT_DB_ID}/query"
response = requests.post(url, headers=headers, json={"page_size": 100})
if response.status_code == 200:
results = response.json().get("results", [])
for p in results:
p_id = p["id"]
# Name property might be "Name" or "Product Category"
props = p["properties"]
name = "Unknown"
if "Name" in props:
name = props["Name"]["title"][0]["plain_text"] if props["Name"]["title"] else "N/A"
elif "Product Category" in props:
name = props["Product Category"]["title"][0]["plain_text"] if props["Product Category"]["title"] else "N/A"
product_map[p_id] = name
# Also map the page ID itself if used in relations
else:
print(f"Error fetching products: {response.status_code}")
# 2. Check Verticals with Relation Resolution
def check_vertical_relations(search_term):
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"contains": search_term
}
}
}
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code == 200:
results = resp.json().get("results", [])
if not results:
print(f"❌ No vertical found for '{search_term}'")
return
for page in results:
props = page["properties"]
title = props["Vertical"]["title"][0]["plain_text"]
# Resolve Primary
pp_ids = [r["id"] for r in props.get("Primary Product Category", {}).get("relation", [])]
pp_names = [product_map.get(pid, pid) for pid in pp_ids]
# Resolve Secondary
sp_ids = [r["id"] for r in props.get("Secondary Product", {}).get("relation", [])]
sp_names = [product_map.get(pid, pid) for pid in sp_ids]
print(f"\n🔹 VERTICAL: {title}")
print(f" Primary Product (Rel): {', '.join(pp_names)}")
print(f" Secondary Product (Rel): {', '.join(sp_names)}")
# Pains/Gains short check
pains = props.get("Pains", {}).get("rich_text", [])
print(f" Pains Length: {len(pains[0]['plain_text']) if pains else 0} chars")
else:
print(f"Error fetching vertical: {resp.status_code}")
# Run
print("Fetching Product Map...")
fetch_products()
print(f"Loaded {len(product_map)} products.")
print("\nChecking Verticals...")
targets = ["Hospital", "Hotel", "Logistics", "Manufacturing", "Retail", "Reinigungs", "Dienstleister", "Facility"]
for t in targets:
check_vertical_relations(t)

View File

@@ -0,0 +1,87 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
def get_vertical_details(vertical_name_contains):
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"contains": vertical_name_contains
}
}
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200:
print(f"Error: {response.status_code}")
return
results = response.json().get("results", [])
if not results:
print(f"❌ No entry found containing '{vertical_name_contains}'")
return
for page in results:
props = page["properties"]
# safely extract title
title_list = props.get("Vertical", {}).get("title", [])
title = title_list[0]["plain_text"] if title_list else "Unknown Title"
# Pains
pains_list = props.get("Pains", {}).get("rich_text", [])
pains = pains_list[0]["plain_text"] if pains_list else "N/A"
# Gains
gains_list = props.get("Gains", {}).get("rich_text", [])
gains = gains_list[0]["plain_text"] if gains_list else "N/A"
# Ops Focus
ops_focus = props.get("Ops Focus: Secondary", {}).get("checkbox", False)
# Products
# Primary is select
pp_select = props.get("Primary Product Category", {}).get("select")
pp = pp_select["name"] if pp_select else "N/A"
# Secondary is select
sp_select = props.get("Secondary Product", {}).get("select")
sp = sp_select["name"] if sp_select else "N/A"
print(f"\n🔹 VERTICAL: {title}")
print(f" Primary: {pp}")
print(f" Secondary: {sp}")
print(f" Ops Focus Secondary? {'✅ YES' if ops_focus else '❌ NO'}")
print(f" PAINS:\n {pains}")
print(f" GAINS:\n {gains}")
print("-" * 40)
targets = [
"Hospital",
"Hotel",
"Logistics",
"Manufacturing",
"Retail",
"Facility Management"
]
for t in targets:
get_vertical_details(t)

View File

@@ -0,0 +1,38 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
# Check for API Key
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
if not NOTION_API_KEY:
try:
with open("/app/n8n_api_Token_git.txt", "r") as f:
content = f.read()
if "secret_" in content:
NOTION_API_KEY = content.strip().split('\n')[0]
except:
pass
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
headers = {"Authorization": f"Bearer {NOTION_API_KEY}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"}
def list_db_properties():
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}"
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
props = resp.json().get("properties", {})
print("Database Properties:")
for name, data in props.items():
print(f"- {name} (Type: {data['type']})")
else:
print(f"Error getting DB: {resp.text}")
if __name__ == "__main__":
list_db_properties()

View File

@@ -0,0 +1,66 @@
import os
import requests
import json
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found in environment.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
def list_pages_and_keys():
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {
"page_size": 10 # Just list a few to see structure
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200:
print(f"Error fetching data: {response.status_code} - {response.text}")
return
results = response.json().get("results", [])
if not results:
print("No pages found.")
return
print(f"Found {len(results)} pages.")
# Print keys from the first page
first_page = results[0]
props = first_page["properties"]
print("\n--- Property Keys Found ---")
for key in props.keys():
print(f"- {key}")
print("\n--- Page Titles (Verticals) ---")
for page in results:
title_prop = page["properties"].get("Vertical", {}).get("title", []) # Assuming title prop is named "Vertical" based on user input
if not title_prop:
# Try finding the title property dynamically if "Vertical" is wrong
for k, v in page["properties"].items():
if v["id"] == "title":
title_prop = v["title"]
break
if title_prop:
title = title_prop[0]["plain_text"]
print(f"- {title}")
else:
print("- (No Title)")
if __name__ == "__main__":
list_pages_and_keys()

View File

@@ -0,0 +1,89 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
# COMPLETE LIST OF UPDATES
updates = {
"Infrastructure - Transport": { # Airports, Stations
"Pains": "Sicherheitsbereiche erfordern personalintensives Screening von externen Reinigungskräften. Verschmutzte Böden (Winter/Salz) erhöhen das Rutschrisiko für Passagiere und Klagerisiken.",
"Gains": "Autonome Reinigung innerhalb der Sicherheitszonen ohne externe Personalwechsel. Permanente Trocknung von Nässe (Schneematsch) in Eingangsbereichen."
},
"Leisure - Indoor Active": { # Bowling, Cinema, Gym
"Pains": "Personal ist rar und teuer, Gäste erwarten aber Service am Platz. Reinigung im laufenden Betrieb stört den Erlebnischarakter.",
"Gains": "Service-Roboter als Event-Faktor und Entlastung: Getränke kommen zum Gast, Personal bleibt an der Bar/Theke. Konstante Sauberkeit auch bei hoher Frequenz."
},
"Leisure - Outdoor Park": { # Zoos, Theme Parks
"Pains": "Enorme Flächenleistung (Wege) erfordert viele Arbeitskräfte für die Grobschmutzbeseitigung (Laub, Müll). Sichtbare Reinigungstrupps stören die Immersion der Gäste.",
"Gains": "Autonome Großflächenreinigung (Kehren) in den frühen Morgenstunden vor Parköffnung. Erhalt der 'heilen Welt' (Immersion) für Besucher."
},
"Leisure - Wet & Spa": { # Pools, Thermen
"Pains": "Hohes Unfallrisiko durch Nässe auf Fliesen (Rutschgefahr). Hoher Aufwand für permanente Desinfektion und Trocknung im laufenden Betrieb bindet Aufsichtspersonal.",
"Gains": "Permanente Trocknung und Desinfektion kritischer Barfußbereiche. Reduktion der Rutschgefahr und Haftungsrisiken. Entlastung der Bademeister (Fokus auf Aufsicht)."
},
"Retail - Shopping Center": { # Malls
"Pains": "Food-Court ist der Schmutz-Hotspot: Verschüttete Getränke und Essensreste wirken unhygienisch und binden Personal dauerhaft. Dreckige Böden senken die Verweildauer.",
"Gains": "Sofortige Beseitigung von Malheuren im Food-Court. Steigerung der Aufenthaltsqualität und Verweildauer der Kunden durch sichtbare Sauberkeit."
},
"Retail - Non-Food": { # DIY, Furniture
"Pains": "Riesige Gangflächen verstauben schnell, Personal ist knapp und soll beraten, nicht kehren. Verschmutzte Böden wirken im Premium-Segment (Möbel) wertmindernd.",
"Gains": "Staubfreie Umgebung für angenehmes Einkaufsklima. Roboter reinigen autonom große Flächen, während Mitarbeiter für Kundenberatung verfügbar sind."
},
"Infrastructure - Public": { # Fairs, Schools
"Pains": "Extrem kurze Turnaround-Zeiten zwischen Messetagen oder Events. Hohe Nachtzuschläge für die Endreinigung der Hallengänge oder Klassenzimmer.",
"Gains": "Automatisierte Nachtreinigung der Gänge/Flure stellt die Optik für den nächsten Morgen sicher. Kalkulierbare Kosten ohne Nachtzuschlag."
},
"Hospitality - Gastronomy": { # Restaurants
"Pains": "Servicepersonal verbringt Zeit auf Laufwegen statt am Gast ('Teller-Taxi'). Personalmangel führt zu langen Wartezeiten und Umsatzverlust.",
"Gains": "Servicekräfte werden von Laufwegen befreit und haben Zeit für aktive Beratung und Verkauf (Upselling). Steigerung der Tischumschlagshäufigkeit."
}
}
def update_vertical(vertical_name, new_data):
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"contains": vertical_name
}
}
}
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code != 200: return
results = resp.json().get("results", [])
if not results:
print(f"Skipping {vertical_name} (Not found)")
return
page_id = results[0]["id"]
update_url = f"https://api.notion.com/v1/pages/{page_id}"
update_payload = {
"properties": {
"Pains": {"rich_text": [{"text": {"content": new_data["Pains"]}}]},
"Gains": {"rich_text": [{"text": {"content": new_data["Gains"]}}]}
}
}
requests.patch(update_url, headers=headers, json=update_payload)
print(f"✅ Updated {vertical_name}")
print("Starting FULL Notion Update...")
for v_name, data in updates.items():
update_vertical(v_name, data)
print("Done.")

View File

@@ -0,0 +1,94 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
# Define the updates with "Sharp" Pains/Gains
updates = {
"Healthcare - Hospital": {
"Pains": "Fachpflegekräfte sind bis zu 30% der Schichtzeit mit logistischen Routinetätigkeiten (Wäsche, Essen, Laborproben) gebunden ('Hände weg vom Bett'). Steigende Hygienerisiken bei gleichzeitigem Personalmangel im Reinigungsteam führen zu lückenhafter Dokumentation und Gefährdung der RKI-Konformität.",
"Gains": "Rückgewinnung von ca. 2,5h Fachkraft-Kapazität pro Schicht durch automatisierte Stationslogistik. Validierbare, RKI-konforme Reinigungsqualität rund um die Uhr, unabhängig vom Krankenstand des Reinigungsteams."
},
"Hospitality - Hotel": {
"Pains": "Enorme Fluktuation im Housekeeping gefährdet die pünktliche Zimmer-Freigabe (Check-in 15:00 Uhr). Hohe Nachtzuschläge oder fehlendes Personal verhindern, dass die Lobby und Konferenzbereiche morgens um 06:00 Uhr perfekt glänzen.",
"Gains": "Lautlose Nachtreinigung der Lobby und Flure ohne Personalzuschläge. Servicekräfte im Restaurant werden von Laufwegen ('Teller-Taxi') befreit und haben Zeit für aktives Upselling am Gast."
},
"Logistics - Warehouse": {
"Pains": "Verschmutzte Fahrwege durch Palettenabrieb und Staub gefährden die Sensorik von FTS (Fahrerlosen Transportsystemen) und erhöhen das Unfallrisiko für Flurförderzeuge. Manuelle Reinigung stört den 24/7-Betrieb und bindet Fachpersonal.",
"Gains": "Permanente Staubreduktion im laufenden Betrieb schützt empfindliche Anlagentechnik (Lichtschranken). Saubere Hallen als Visitenkarte und Sicherheitsfaktor (Rutschgefahr), ohne operative Unterbrechungen."
},
"Industry - Manufacturing": {
"Pains": "Hochbezahlte Facharbeiter unterbrechen die Wertschöpfung für unproduktive Such- und Holzeiten von Material (C-Teile). Intransparente Materialflüsse an der Linie führen zu Mikrostillständen und gefährden die Taktzeit.",
"Gains": "Just-in-Time Materialversorgung direkt an die Linie. Fachkräfte bleiben an der Maschine. Stabilisierung der Taktzeiten und OEE durch automatisierten Nachschub."
},
"Reinigungsdienstleister": { # Facility Management
"Pains": "Margendruck durch steigende Tariflöhne bei gleichzeitigem Preisdiktat der Auftraggeber. Hohe Fluktuation (>30%) führt zu ständiger Rekrutierung ('No-Show'-Quote), was Objektleiter bindet und die Qualitätskontrolle vernachlässigt.",
"Gains": "Kalkulationssicherheit durch Fixkosten statt variabler Personalkosten. Garantierte Reinigungsleistung in Objekten unabhängig vom Personalstand. Innovationsträger für Ausschreibungen."
},
"Retail - Food": { # Supermarkets
"Pains": "Reinigungskosten steigen linear zur Fläche, während Kundenfrequenz schwankt. Sichtbare Reinigungsmaschinen blockieren tagsüber Kundenwege ('Störfaktor'). Abends/Nachts schwer Personal zu finden.",
"Gains": "Unsichtbare Reinigung: Roboter fahren in Randzeiten oder weichen Kunden dynamisch aus. Konstantes Sauberkeits-Level ('Lobby-Effekt') steigert Verweildauer."
}
}
def update_vertical(vertical_name, new_data):
# 1. Find Page ID
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"contains": vertical_name
}
}
}
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code != 200:
print(f"Error searching {vertical_name}: {resp.status_code}")
return
results = resp.json().get("results", [])
if not results:
print(f"Skipping {vertical_name} (Not found)")
return
page_id = results[0]["id"]
# 2. Update Page
update_url = f"https://api.notion.com/v1/pages/{page_id}"
update_payload = {
"properties": {
"Pains": {
"rich_text": [{"text": {"content": new_data["Pains"]}}]
},
"Gains": {
"rich_text": [{"text": {"content": new_data["Gains"]}}]
}
}
}
upd_resp = requests.patch(update_url, headers=headers, json=update_payload)
if upd_resp.status_code == 200:
print(f"✅ Updated {vertical_name}")
else:
print(f"❌ Failed to update {vertical_name}: {upd_resp.text}")
print("Starting Notion Update...")
for v_name, data in updates.items():
update_vertical(v_name, data)
print("Done.")

View File

@@ -0,0 +1,194 @@
import os
import requests
import json
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_VERTICALS = "2ec88f4285448014ab38ea664b4c2b81"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
# The approved changes from ANALYSIS_AND_PROPOSAL.md
UPDATES = {
"Automotive - Dealer": {
"Pains": """[Primary Product: Security]
- Teile-Diebstahl: Organisierte Banden demontieren nachts Katalysatoren und Räder enormer Schaden und Versicherungsstress.
- Vandalismus: Zerkratzte Neuwagen auf dem Außenhof mindern den Verkaufswert drastisch.
- Personalkosten: Lückenlose menschliche Nachtbewachung ist für viele Standorte wirtschaftlich kaum darstellbar.
[Secondary Product: Cleaning Outdoor]
- Image-Verlust: Ein verschmutzter Außenbereich (Laub, Müll) passt nicht zum Premium-Anspruch der ausgestellten Fahrzeuge.
- Manueller Aufwand: Verkaufspersonal oder teure Hausmeisterdienste binden Zeit mit unproduktivem Fegen.""",
"Gains": """[Primary Product: Security]
- Abschreckung & Intervention: Permanente Roboter-Präsenz wirkt präventiv; bei Alarm schaltet sich sofort eine Leitstelle auf.
- Asset-Schutz: Reduktion von Versicherungsschäden und Selbstbehalten durch lückenlose Dokumentation.
[Secondary Product: Cleaning Outdoor]
- Premium-Präsentation: Der Hof ist bereits morgens bei Kundenöffnung makellos sauber.
- Automatisierung: Täglich gereinigte Flächen ohne manuellen Eingriff."""
},
"Industry - Manufacturing": {
"Pains": """[Primary Product: Cleaning Indoor]
- Prozess-Sicherheit: Staub und Abrieb auf Fahrwegen gefährden empfindliche Sensorik (z.B. von FTS) und die Produktqualität.
- Arbeitssicherheit: Rutschgefahr durch feine Staubschichten oder ausgelaufene (nicht-chemische) Flüssigkeiten erhöht das Unfallrisiko.
- Ressourcen-Verschwendung: Hochbezahlte Fachkräfte müssen Maschinen stoppen, um ihr Umfeld zu reinigen.
[Secondary Product: Transport]
- Intransparenz & Suchzeiten: Facharbeiter unterbrechen die Wertschöpfung für unproduktive Materialbeschaffung ("C-Teile holen").
- Mikrostillstände: Fehlendes Material an der Linie stoppt den Takt.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Konstante Bodenqualität: Definierte Sauberkeitsstandards (Audit-Ready) rund um die Uhr.
- Unfallschutz: Reduktion von Arbeitsunfällen durch rutschfreie Verkehrswege.
[Secondary Product: Transport]
- Just-in-Time Logistik: Automatisierter Nachschub hält die Fachkraft wertschöpfend an der Maschine.
- Fluss-Optimierung: Stabilisierung der Taktzeiten und OEE durch verlässliche Materialflüsse."""
},
"Healthcare - Hospital": {
"Pains": """[Primary Product: Cleaning Indoor]
- Hygienerisiko & Kreuzkontamination: Manuelle Reinigung ist oft fehleranfällig und variiert stark in der Qualität (Gefahr für Patienten).
- Dokumentationspflicht: Der Nachweis RKI-konformer Reinigung bindet wertvolle Zeit und ist bei Personalmangel lückenhaft.
- Personalnot: Fehlende Reinigungskräfte führen zu gesperrten Bereichen oder sinkendem Hygienelevel.
[Secondary Product: Service]
- Berufsfremde Tätigkeiten: Pflegekräfte verbringen bis zu 30% der Schichtzeit mit Hol- und Bringdiensten (Essen, Wäsche, Labor).
- Physische Überlastung: Lange Laufwege in großen Kliniken erhöhen die Erschöpfung des Fachpersonals.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Validierbare Hygiene: Robotergarantierte, protokollierte Desinfektionsleistung audit-sicher auf Knopfdruck.
- 24/7 Verfügbarkeit: Konstantes Hygienelevel auch nachts und am Wochenende, unabhängig vom Dienstplan.
[Secondary Product: Service]
- Zeit für Patienten: Rückgewinnung von ca. 2,5 Stunden Fachkraft-Kapazität pro Schicht für die Pflege.
- Mitarbeiterzufriedenheit: Reduktion der Laufwege ("Schrittzähler") entlastet das Team spürbar."""
},
"Logistics - Warehouse": {
"Pains": """[Primary Product: Cleaning (Sweeper/Dry)]
- Grobschmutz & Palettenreste: Holzspäne und Verpackungsreste gefährden Reifen von Flurförderzeugen und blockieren Lichtschranken.
- Staubbelastung: Aufgewirbelter Staub legt sich auf Waren und Verpackungen (Reklamationsgrund) und schadet der Gesundheit.
- Manuelle Bindung: Mitarbeiter müssen große Flächen manuell kehren, statt zu kommissionieren.
[Secondary Product: Cleaning (Wet)]
- Hartnäckige Verschmutzungen: Eingefahrene Spuren, die durch reines Kehren nicht lösbar sind.""",
"Gains": """[Primary Product: Cleaning (Sweeper/Dry)]
- Anlagenschutz: Sauberer Boden verhindert Störungen an Fördertechnik und Sensoren durch Staub/Teile.
- Staubfreie Ware: Produkte verlassen das Lager in sauberem Zustand (Qualitätsanspruch).
[Secondary Product: Cleaning (Wet)]
- Grundsauberkeit: Gelegentliche Nassreinigung für Tiefenhygiene in Fahrgassen."""
},
"Retail - Food": {
"Pains": """[Primary Product: Cleaning Indoor]
- "Malheur-Management": Zerbrochene Gläser oder ausgelaufene Flüssigkeiten (Haverien) bilden sofortige Rutschfallen und binden Personal.
- Optischer Eindruck: Grauschleier und verschmutzte Böden senken das Frische-Empfinden der Kunden massiv.
- Personal-Engpass: Marktpersonal soll Regale füllen und kassieren, nicht mit der Scheuersaugmaschine fahren.
[Secondary Product: Service]
- Fehlende Beratung: Kunden finden Produkte nicht und brechen den Kauf ab, da kein Personal greifbar ist.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Sofortige Sicherheit: Roboter beseitigt Rutschgefahren autonom und schnell.
- Frische-Optik: Permanent glänzende Böden ("Lobby-Effekt") unterstreichen die Qualität der Lebensmittel.
[Secondary Product: Service]
- Umsatz-Boost: Roboter führt Kunden direkt zum gesuchten Produkt oder bewirbt Aktionen aktiv am POS."""
},
"Hospitality - Gastronomy": {
"Pains": """[Primary Product: Cleaning Indoor]
- Klebrige Böden: Verschüttete Getränke und Speisereste wirken unhygienisch und stören das Ambiente.
- Randzeiten-Problem: Nach Schließung ist es schwer, Personal für die Grundreinigung zu finden (Nachtzuschläge).
[Secondary Product: Service]
- "Teller-Taxi": Servicekräfte verbringen 80% der Zeit mit Laufen (Küche <-> Gast) statt mit Verkaufen/Betreuung.
- Personalmangel: Zu wenig Kellner führen zu langen Wartezeiten, kalten Speisen und genervten Gästen.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Makelloses Ambiente: Sauberer Boden als Visitenkarte des Restaurants.
- Zuverlässigkeit: Die Grundreinigung findet jede Nacht garantiert statt.
[Secondary Product: Service]
- Mehr Umsatz am Gast: Servicekraft hat Zeit für Empfehlungen (Wein, Dessert) und Upselling.
- Entlastung: Roboter übernimmt das schwere Tragen (Tabletts), Personal bleibt im Gastraum präsent."""
},
"Leisure - Outdoor Park": {
"Pains": """[Primary Product: Cleaning Outdoor]
- Immersion-Breaker: Müll und Laub auf den Wegen stören die perfekte Illusion ("Heile Welt") des Parks.
- Enorme Flächen: Kilometerlange Wegenetze binden ganze Kolonnen von Reinigungskräften.
- Sicherheit: Rutschgefahr durch nasses Laub oder Abfall.
[Secondary Product: Service]
- Versorgungslücken: An abgelegenen Attraktionen fehlt oft Gastronomie-Angebot.""",
"Gains": """[Primary Product: Cleaning Outdoor]
- Perfekte Inszenierung: Unsichtbare Reinigung in den frühen Morgenstunden sichert das perfekte Erlebnis bei Parköffnung.
- Effizienz: Ein Roboter schafft die Flächenleistung mehrerer manueller Kehrer.
[Secondary Product: Service]
- Mobiler Verkauf: Roboter bringen Getränke/Eis direkt zu den Warteschlangen (Zusatzumsatz)."""
},
"Energy - Grid & Utilities": {
"Pains": """[Primary Product: Security]
- Sabotage & Diebstahl: Kupferdiebstahl in Umspannwerken verursacht Millionenschäden und Versorgungsausfälle.
- Reaktionszeit: Entlegene Standorte sind für Interventionskräfte oft zu spät erreichbar.
- Sicherheitsrisiko Mensch: Alleinarbeit bei Kontrollgängen in Hochspannungsbereichen ist gefährlich.""",
"Gains": """[Primary Product: Security]
- First Responder Maschine: Roboter ist bereits vor Ort, verifiziert Alarm und schreckt Täter ab.
- KRITIS-Compliance: Lückenlose, manipulationssichere Dokumentation aller Vorfälle für Behörden.
- Arbeitsschutz: Roboter übernimmt gefährliche Routinekontrollen (z.B. Thermografie an Trafos)."""
}
}
def get_page_id(vertical_name):
url = f"https://api.notion.com/v1/databases/{NOTION_DB_VERTICALS}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"equals": vertical_name
}
}
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
results = response.json().get("results", [])
if results:
return results[0]["id"]
return None
def update_page(page_id, pains, gains):
url = f"https://api.notion.com/v1/pages/{page_id}"
payload = {
"properties": {
"Pains": {
"rich_text": [{"text": {"content": pains}}]
},
"Gains": {
"rich_text": [{"text": {"content": gains}}]
}
}
}
response = requests.patch(url, headers=headers, json=payload)
if response.status_code == 200:
print(f"✅ Updated {page_id}")
else:
print(f"❌ Failed to update {page_id}: {response.text}")
def main():
print("Starting update...")
for vertical, content in UPDATES.items():
print(f"Processing '{vertical}'...")
page_id = get_page_id(vertical)
if page_id:
update_page(page_id, content["Pains"], content["Gains"])
else:
print(f"⚠️ Vertical '{vertical}' not found in Notion.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,162 @@
import os
import requests
import json
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
NOTION_DB_VERTICALS = "2ec88f4285448014ab38ea664b4c2b81"
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
# The approved changes from ANALYSIS_AND_PROPOSAL.md for Phase 2
UPDATES = {
"Energy - Solar/Wind": {
"Pains": """[Primary Product: Security]
- Kupfer-Diebstahl: Professionelle Banden plündern abgelegene Parks in Minuten; der Schaden durch Betriebsunterbrechung übersteigt den Materialwert oft weit.
- Interventionszeit: Bis der Wachdienst eintrifft ("Blaulicht-Fahrt"), sind die Täter längst verschwunden.
- Kostenfalle Falschalarm: Wildtiere oder wetterbedingte Störungen lösen teure, unnötige Polizeieinsätze aus.""",
"Gains": """[Primary Product: Security]
- Sofort-Verifikation: KI-gestützte Erkennung unterscheidet zuverlässig zwischen Tier und Mensch und liefert Live-Bilder in Sekunden.
- Präventive Abschreckung: Autonome Patrouillen signalisieren "Hier wird bewacht" und verhindern den Versuch.
- Lückenlose Beweissicherung: Gerichtsfeste Dokumentation von Vorfällen für Versicherung und Strafverfolgung."""
},
"Infrastructure - Public": {
"Pains": """[Primary Product: Cleaning Indoor]
- Zeitdruck (Turnaround): Zwischen Messe-Ende und Öffnung am nächsten Tag liegen nur wenige Stunden für eine Komplettreinigung.
- Kostenspirale: Nacht- und Wochenendzuschläge für manuelles Personal belasten das Budget massiv.
- Personalverfügbarkeit: Für Spitzenlasten (Messezeiten) ist kurzfristig kaum ausreichendes Personal zu finden.
[Secondary Product: Cleaning Outdoor]
- Erster Eindruck: Vermüllte Vorplätze und Zufahrten schaden dem Image der Veranstaltung schon bei Ankunft.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Planbare Kapazität: Roboter reinigen autonom die Kilometer langen Gänge ("Gang-Reinigung"), Personal fokussiert sich auf Stände und Details.
- Kosteneffizienz: Fixe Kosten statt variabler Zuschläge für Nachtarbeit.
[Secondary Product: Cleaning Outdoor]
- Repräsentative Außenwirkung: Sauberer Empfangsbereich ohne permanenten Personaleinsatz."""
},
"Infrastructure - Transport": {
"Pains": """[Primary Product: Cleaning Indoor]
- Sicherheits-Checks: Jede externe Reinigungskraft im Sicherheitsbereich erfordert aufwändige Überprüfungen (ZÜP) und Begleitung.
- Passagier-Störung: Laute, manuelle Reinigungsmaschinen behindern Laufwege und Durchsagen im 24/7-Betrieb.
- Hochfrequenz-Verschmutzung: Kaffee-Flecken und Nässe (Winter) müssen sofort beseitigt werden, um Rutschunfälle zu vermeiden.
[Secondary Product: Cleaning Outdoor]
- Müll-Aufkommen: Raucherbereiche und Taxi-Spuren verkommen schnell durch Zigarettenstummel und Kleinmüll.""",
"Gains": """[Primary Product: Cleaning Indoor]
- "Approved Staff": Roboter verbleibt im Sicherheitsbereich kein täglicher Check-in/Check-out nötig.
- Silent Cleaning: Leise, autonome Navigation zwischen Passagieren stört den Betriebsablauf nicht.
[Secondary Product: Cleaning Outdoor]
- Sauberer Transfer: Gepflegte Außenanlagen als Visitenkarte der Mobilitätsdrehscheibe."""
},
"Retail - Shopping Center": {
"Pains": """[Primary Product: Cleaning Indoor]
- Food-Court-Chaos: Zu Stoßzeiten kommen Reinigungskräfte mit dem Wischen von verschütteten Getränken und Essensresten kaum nach.
- Rutschfallen: Nasse Eingänge (Regen) und verschmutzte Zonen sind Haftungsrisiken für den Betreiber.
- Image-Faktor: Ein "grauer" oder fleckiger Boden senkt die Aufenthaltsqualität und damit die Verweildauer der Kunden.
[Secondary Product: Cleaning Outdoor]
- Parkplatz-Pflege: Müll auf Parkplätzen und in Parkhäusern ist der erste negative Touchpoint für Besucher.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Reaktionsschnelligkeit: Roboter sind permanent präsent und beseitigen Malheure sofort, bevor sie antrocknen.
- Hochglanz-Optik: Konstante Pflege poliert den Steinboden und sorgt für ein hochwertiges Ambiente.
[Secondary Product: Cleaning Outdoor]
- Willkommens-Kultur: Sauberer Außenbereich lädt zum Betreten ein."""
},
"Leisure - Wet & Spa": {
"Pains": """[Primary Product: Cleaning Indoor]
- Rutsch-Unfälle: Staunässe auf Fliesen ist die Unfallursache Nummer 1 in Bädern hohes Haftungsrisiko.
- Hygiene-Sensibilität: Im Barfußbereich (Umkleiden/Gänge) erwarten Gäste klinische Sauberkeit; Haare und Fussel sind "Ekel-Faktor".
- Personal-Konflikt: Fachangestellte für Bäderbetriebe sollen die Beckenaufsicht führen (Sicherheit), nicht wischen.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Permanente Sicherheit: Roboter trocknen Laufwege kontinuierlich und minimieren das Rutschrisiko aktiv.
- Entlastung der Aufsicht: Bademeister können sich zu 100% auf die Sicherheit der Badegäste konzentrieren.
- Hygiene-Standard: Dokumentierte Desinfektion und Reinigung sichert Top-Bewertungen."""
},
"Corporate - Campus": {
"Pains": """[Primary Product: Cleaning Indoor]
- Repräsentativität: Empfangshallen und Atrien sind das Aushängeschild sichtbarer Staub oder Schlieren wirken unprofessionell.
- Kostendruck Facility: Enorme Flächen (Flure/Verbindungsgänge) erzeugen hohe laufende Reinigungskosten.
[Secondary Product: Cleaning Outdoor]
- Campus-Pflege: Weitläufige Außenanlagen manuell sauber zu halten, bindet unverhältnismäßig viele Ressourcen.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Innovations-Statement: Einsatz von Robotik unterstreicht den technologischen Führungsanspruch des Unternehmens gegenüber Besuchern und Bewerbern.
- Konstante Qualität: Einheitliches Sauberkeitsniveau in allen Gebäudeteilen, unabhängig von Tagesform oder Krankenstand.
[Secondary Product: Cleaning Outdoor]
- Gepflegtes Erscheinungsbild: Automatisierte Kehrleistung sorgt für repräsentative Wege und Plätze."""
},
"Reinigungsdienstleister": {
"Pains": """[Primary Product: Cleaning Indoor]
- Personal-Mangel & Fluktuation: Hohe "No-Show"-Quoten und ständige Neurekrutierung binden Objektleiter massiv und gefährden die Vertragserfüllung.
- Margen-Verfall: Steigende Tariflöhne bei gleichzeitigem Preisdruck der Auftraggeber lassen kaum noch Gewinn zu.
- Qualitäts-Schwankungen: Wechselndes, ungelernte Personal liefert oft unzureichende Ergebnisse, was zu Reklamationen und Kürzungen führt.""",
"Gains": """[Primary Product: Cleaning Indoor]
- Kalkulations-Sicherheit: Roboter bieten fixe Kosten statt unkalkulierbarer Krankheits- und Ausfallrisiken.
- Wettbewerbsvorteil: Mit Robotik-Konzepten punkten Dienstleister bei Ausschreibungen als Innovationsführer.
- Entlastung Objektleitung: Weniger Personal-Management bedeutet mehr Zeit für Kundenpflege und Qualitätskontrolle."""
}
}
def get_page_id(vertical_name):
# Try to find the page with a filter on "Vertical" property
url = f"https://api.notion.com/v1/databases/{NOTION_DB_VERTICALS}/query"
payload = {
"filter": {
"property": "Vertical",
"title": {
"equals": vertical_name
}
}
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
results = response.json().get("results", [])
if results:
return results[0]["id"]
return None
def update_page(page_id, pains, gains):
url = f"https://api.notion.com/v1/pages/{page_id}"
payload = {
"properties": {
"Pains": {
"rich_text": [{"text": {"content": pains}}]
},
"Gains": {
"rich_text": [{"text": {"content": gains}}]
}
}
}
response = requests.patch(url, headers=headers, json=payload)
if response.status_code == 200:
print(f"✅ Updated {page_id}")
else:
print(f"❌ Failed to update {page_id}: {response.text}")
def main():
print("Starting update Phase 2...")
for vertical, content in UPDATES.items():
print(f"Processing '{vertical}'...")
page_id = get_page_id(vertical)
if page_id:
update_page(page_id, content["Pains"], content["Gains"])
else:
print(f"⚠️ Vertical '{vertical}' not found in Notion.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,97 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
# Check for API Key
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
if not NOTION_API_KEY:
try:
with open("/app/n8n_api_Token_git.txt", "r") as f:
content = f.read()
if "secret_" in content:
NOTION_API_KEY = content.strip().split('\n')[0]
except:
pass
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
headers = {"Authorization": f"Bearer {NOTION_API_KEY}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"}
# Updates Definition
updates = {
"Energy - Grid & Utilities": {
"Pains": "[Primary Product: Security]\n- Sabotage & Diebstahl: Kupferdiebstahl in Umspannwerken verursacht Millionenschäden und Versorgungsausfälle.\n- Reaktionszeit: Entlegene Standorte sind für Interventionskräfte oft zu spät erreichbar.\n- Sicherheitsrisiko Mensch: Alleinarbeit bei Kontrollgängen in Hochspannungsbereichen ist gefährlich.\n\n[Secondary Product: Cleaning Indoor]\n- Verschmutzung in Umspannwerken: Staubablagerungen auf Böden und in technischen Bereichen können die Betriebssicherheit gefährden.\n- Manuelle Reinigung in Sicherheitsbereichen: Externes Reinigungspersonal benötigt aufwändige Sicherheitsunterweisungen und Begleitung.\n- Große Distanzen: Die Reinigung weitläufiger, oft unbemannter Anlagen ist logistisch aufwändig und wird häufig vernachlässigt.",
"Gains": "[Primary Product: Security]\n- First Responder Maschine: Roboter ist bereits vor Ort, verifiziert Alarm und schreckt Täter ab.\n- KRITIS-Compliance: Lückenlose, manipulationssichere Dokumentation aller Vorfälle für Behörden.\n- Arbeitsschutz: Roboter übernimmt gefährliche Routinekontrollen (z.B. Thermografie an Trafos).\n\n[Secondary Product: Cleaning Indoor]\n- Permanente Sauberkeit: Autonome Reinigung gewährleistet staubfreie Böden und reduziert das Risiko von technischen Störungen.\n- Zugang ohne Sicherheitsrisiko: Der Roboter ist \"Teil der Anlage\" und benötigt keine externe Sicherheitsfreigabe oder Begleitung.\n- Ressourceneffizienz: Kosteneffiziente Reinigung großer Flächen ohne Anreisezeiten für Dienstleister.",
"Secondary_Product_Name": "Cleaning Indoor (Wet Surface)"
},
"Retail - Non-Food": {
"Pains": "[Primary Product: Cleaning Indoor]\n- Optischer Eindruck: Verschmutzte Böden, insbesondere im Premium-Segment (Möbel, Elektronik), mindern die Wertwahrnehmung der ausgestellten Produkte massiv.\n- Staubentwicklung auf großen Flächen: In Möbelhäusern und Baumärkten sammelt sich auf den riesigen Gangflächen schnell Staub, der das Einkaufserlebnis trübt.\n- Personalbindung: Verkaufsberater sollen Kunden betreuen und Umsatz generieren, statt wertvolle Zeit mit unproduktiven Kehr- oder Wischtätigkeiten zu verbringen.\n\n[Secondary Product: Service]\n- Unübersichtlichkeit: Kunden finden in großen Märkten oft nicht sofort das gesuchte Produkt und binden Personal für einfache Wegbeschreibungen.\n- Fehlende Interaktion: Passive Verkaufsflächen bieten wenig Anreiz für Kunden, sich länger aufzuhalten oder zu interagieren.",
"Gains": "[Primary Product: Cleaning Indoor]\n- Perfektes Einkaufserlebnis: Stets makellos saubere Böden unterstreichen den Qualitätsanspruch des Sortiments und laden zum Verweilen ein.\n- Fokus auf Beratung: Mitarbeiter werden von routinemäßigen Reinigungsaufgaben befreit und können sich voll auf den Kunden und den Verkauf konzentrieren.\n- Kosteneffizienz auf der Fläche: Autonome Reinigung großer Quadratmeterzahlen ist deutlich günstiger als manuelle Arbeit, besonders außerhalb der Öffnungszeiten.\n\n[Secondary Product: Service]\n- Innovativer Kundenservice: Roboter führen Kunden autonom zum gesuchten Produktregal (\"Guide-Funktion\").\n- Wow-Effekt: Der Einsatz von Robotik modernisiert das Markenimage und zieht Aufmerksamkeit auf sich."
},
"Tech - Data Center": {
"Pains": "[Primary Product: Security]\n- Sicherheitsrisiko Zutritt: Unbefugter Zutritt in Hochsicherheitsbereiche (Serverräume, Cages) muss lückenlos detektiert und dokumentiert werden, um Zertifizierungen (ISO 27001) nicht zu gefährden.\n- Fachkräftemangel Security: Qualifiziertes Wachpersonal mit Sicherheitsüberprüfung ist extrem schwer zu finden und teuer im 24/7-Schichtbetrieb.\n- Dokumentationslücken: Manuelle Patrouillen sind fehleranfällig und Protokolle können unvollständig sein, was bei Audits zu Problemen führt.\n\n[Secondary Product: Cleaning Indoor]\n- Gefahr durch Staubpartikel: Feinstaub in Serverräumen kann Kühlsysteme verstopfen und Kurzschlüsse verursachen, was die Hardware-Lebensdauer verkürzt.\n- Sicherheitsrisiko Reinigungspersonal: Externes Reinigungspersonal in Sicherheitsbereichen erfordert ständige Begleitung und Überwachung (Vier-Augen-Prinzip), was Personal bindet.",
"Gains": "[Primary Product: Security]\n- Lückenloser Audit-Trail: Automatisierte, manipulationssichere Dokumentation aller Kontrollgänge und Ereignisse sichert Compliance-Anforderungen.\n- 24/7 Präsenz: Der Roboter ist immer im Dienst, wird nicht müde und garantiert eine konstante Überwachungsqualität ohne Schichtwechsel-Risiken.\n- Sofortige Alarmierung: Bei Anomalien (offene Rack-Tür, Wärmeentwicklung) erfolgt eine Echtzeit-Meldung an die Leitzentrale.\n\n[Secondary Product: Cleaning Indoor]\n- Maximale Hardware-Verfügbarkeit: Staubfreie Umgebung optimiert die Kühleffizienz und reduziert das Ausfallrisiko teurer Komponenten.\n- Autonome \"Trusted\" Cleaning: Der Roboter reinigt sensibelste Bereiche ohne das Risiko menschlichen Fehlverhaltens oder unbefugten Zugriffs.",
"Secondary_Product_Name": "Cleaning Indoor (Wet Surface)"
}
}
def get_product_page_id(product_name):
url = "https://api.notion.com/v1/search"
payload = {"query": product_name, "filter": {"value": "page", "property": "object"}}
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code == 200:
results = resp.json().get("results", [])
if results: return results[0]["id"]
return None
def update_vertical(vertical_name, new_data):
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {"filter": {"property": "Vertical", "title": {"contains": vertical_name}}}
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code != 200:
print(f"Error searching for {vertical_name}: {resp.text}")
return
results = resp.json().get("results", [])
if not results:
print(f"Skipping {vertical_name} (Not found)")
return
page_id = results[0]["id"]
print(f"Found {vertical_name} (ID: {page_id})")
props_update = {
"Pains": {"rich_text": [{"text": {"content": new_data["Pains"]}}],},
"Gains": {"rich_text": [{"text": {"content": new_data["Gains"]}}]}
}
if "Secondary_Product_Name" in new_data:
prod_name = new_data["Secondary_Product_Name"]
prod_id = get_product_page_id(prod_name)
if prod_id:
print(f" Found Product ID for '{prod_name}': {prod_id}")
props_update["Secondary Product Category"] = {"relation": [{"id": prod_id}]}
props_update["Ops Focus Secondary"] = {"checkbox": True}
else:
print(f" WARNING: Product '{prod_name}' not found.")
update_url = f"https://api.notion.com/v1/pages/{page_id}"
update_payload = {"properties": props_update}
resp_patch = requests.patch(update_url, headers=headers, json=update_payload)
if resp_patch.status_code == 200:
print(f"✅ Successfully updated {vertical_name}")
else:
print(f"❌ Failed to update {vertical_name}: {resp_patch.text}")
print("Starting Targeted Notion Update...")
for v_name, data in updates.items():
update_vertical(v_name, data)
print("Done.")

View File

@@ -0,0 +1,88 @@
import os
import requests
import json
from dotenv import load_dotenv
load_dotenv()
# Check for API Key
NOTION_API_KEY = os.getenv("NOTION_API_KEY")
if not NOTION_API_KEY:
try:
with open("/app/n8n_api_Token_git.txt", "r") as f:
content = f.read()
if "secret_" in content:
NOTION_API_KEY = content.strip().split('\n')[0]
except:
pass
if not NOTION_API_KEY:
print("Error: NOTION_API_KEY not found.")
exit(1)
NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81"
headers = {"Authorization": f"Bearer {NOTION_API_KEY}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"}
targets = [
"Energy - Grid & Utilities",
"Tech - Data Center",
"Retail - Non-Food"
]
def check_vertical(vertical_name):
print(f"\n--- Checking: {vertical_name} ---")
url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query"
payload = {"filter": {"property": "Vertical", "title": {"contains": vertical_name}}}
resp = requests.post(url, headers=headers, json=payload)
if resp.status_code != 200:
print(f"Error: {resp.text}")
return
results = resp.json().get("results", [])
if not results:
print("Not found.")
return
page = results[0]
props = page["properties"]
# Check Pains (Start)
pains = props.get("Pains", {}).get("rich_text", [])
pains_text = "".join([t["text"]["content"] for t in pains])
print(f"PAINS (First 100 chars): {pains_text[:100]}...")
# Check Gains (Start)
gains = props.get("Gains", {}).get("rich_text", [])
gains_text = "".join([t["text"]["content"] for t in gains])
print(f"GAINS (First 100 chars): {gains_text[:100]}...")
# Check Ops Focus Secondary
ops_focus = props.get("Ops Focus: Secondary", {}).get("checkbox", False)
print(f"Ops Focus Secondary: {ops_focus}")
# Check Secondary Product
sec_prod_rel = props.get("Secondary Product", {}).get("relation", [])
if sec_prod_rel:
prod_id = sec_prod_rel[0]["id"]
# Fetch Product Name
prod_url = f"https://api.notion.com/v1/pages/{prod_id}"
prod_resp = requests.get(prod_url, headers=headers)
if prod_resp.status_code == 200:
prod_props = prod_resp.json()["properties"]
# Try to find Name/Title
# Usually "Name" or "Product Name"
# Let's look for title type
prod_name = "Unknown"
for k, v in prod_props.items():
if v["type"] == "title":
prod_name = "".join([t["text"]["content"] for t in v["title"]])
print(f"Secondary Product: {prod_name}")
else:
print(f"Secondary Product ID: {prod_id} (Could not fetch name)")
else:
print("Secondary Product: None")
for t in targets:
check_vertical(t)

View File

@@ -0,0 +1,91 @@
import requests
import json
import os
TOKEN_FILE = 'notion_api_key.txt'
DATABASE_ID = "2e088f42-8544-815e-a3f9-e226f817bded"
# Data from the VIGGO S100-N analysis
PRODUCT_DATA = {
"specs": {
"metadata": {
"brand": "VIGGO",
"model_name": "S100-N",
"category": "cleaning",
"manufacturer_url": None
},
"core_specs": {
"battery_runtime_min": 360,
"charge_time_min": 270,
"weight_kg": 395.0,
"max_slope_deg": 10.0
},
"layers": {
"cleaning": {
"fresh_water_l": 60.0,
"area_performance_sqm_h": 3000.0
}
}
}
}
def add_to_notion(token):
url = "https://api.notion.com/v1/pages"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
specs = PRODUCT_DATA["specs"]
meta = specs["metadata"]
core = specs["core_specs"]
cleaning = specs["layers"].get("cleaning", {})
properties = {
"Model Name": {"title": [{"text": {"content": meta["model_name"]}}]},
"Brand": {"select": {"name": meta["brand"]}},
"Category": {"select": {"name": meta["category"]}},
"Battery Runtime (min)": {"number": core.get("battery_runtime_min")},
"Charge Time (min)": {"number": core.get("charge_time_min")},
"Weight (kg)": {"number": core.get("weight_kg")},
"Max Slope (deg)": {"number": core.get("max_slope_deg")},
"Fresh Water (l)": {"number": cleaning.get("fresh_water_l")},
"Area Performance (m2/h)": {"number": cleaning.get("area_performance_sqm_h")}
}
# Add URL if present
if meta.get("manufacturer_url"):
properties["Manufacturer URL"] = {"url": meta["manufacturer_url"]}
payload = {
"parent": {"database_id": DATABASE_ID},
"properties": properties
}
print(f"Adding {meta['brand']} {meta['model_name']} to Notion database...")
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
print("\n=== SUCCESS ===")
print(f"Product added to database!")
print(f"Page URL: {data.get('url')}")
except requests.exceptions.HTTPError as e:
print(f"\n=== ERROR ===")
print(f"HTTP Error: {e}")
print(f"Response: {response.text}")
def main():
try:
with open(TOKEN_FILE, 'r') as f:
token = f.read().strip()
except FileNotFoundError:
print(f"Error: Could not find '{TOKEN_FILE}'")
return
add_to_notion(token)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,58 @@
import requests
from getpass import getpass
# Interaktive und sichere Abfrage des Tokens
print("--- Notion API Token Gültigkeits-Check ---")
notion_token = getpass("Bitte gib deinen Notion API Key ein (Eingabe wird nicht angezeigt): ")
if not notion_token:
print("\nFehler: Kein Token eingegeben.")
exit()
# Der einfachste API-Endpunkt, um die Authentifizierung zu testen
url = "https://api.notion.com/v1/users/me"
headers = {
"Authorization": f"Bearer {notion_token}",
"Notion-Version": "2022-06-28"
}
print("\n... Sende Test-Anfrage an Notion...")
try:
# --- TEST 1: Grundlegende Authentifizierung ---
print("\n[TEST 1/2] Prüfe grundlegende Authentifizierung (/users/me)...")
user_response = requests.get("https://api.notion.com/v1/users/me", headers=headers)
user_response.raise_for_status()
print("✅ ERFOLG! Der API Token ist gültig.")
# --- TEST 2: Suche nach der 'Projects' Datenbank ---
print("\n[TEST 2/2] Versuche, die 'Projects'-Datenbank über die Suche zu finden (/search)...")
search_url = "https://api.notion.com/v1/search"
search_payload = {
"query": "Projects",
"filter": {"value": "database", "property": "object"}
}
search_response = requests.post(search_url, headers=headers, json=search_payload)
search_response.raise_for_status()
results = search_response.json().get("results", [])
if not results:
print("🟡 WARNUNG: Die Suche war erfolgreich, hat aber keine Datenbank namens 'Projects' gefunden.")
else:
print("✅✅✅ ERFOLG! Die Suche funktioniert und hat die 'Projects'-Datenbank gefunden.")
print("Gefundene Datenbanken:")
for db in results:
print(f"- ID: {db['id']}, Titel: {db.get('title', [{}])[0].get('plain_text', 'N/A')}")
except requests.exceptions.HTTPError as e:
print(f"\n❌ FEHLER! Einer der Tests ist fehlgeschlagen.")
print(f"URL: {e.request.url}")
print(f"HTTP Status Code: {e.response.status_code}")
print("Antwort von Notion:")
try:
print(e.response.json())
except:
print(e.response.text)
except requests.exceptions.RequestException as e:
print(f"\n❌ FEHLER! Ein Netzwerk- oder Verbindungsfehler ist aufgetreten: {e}")

View File

@@ -0,0 +1,42 @@
import json
from notion_client import Client
# SETUP
TOKEN = "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"
SECTOR_DB_ID = "59a4598a20084ddaa035f5eba750a1be"
notion = Client(auth=TOKEN)
def inspect_via_page():
print(f"🔍 Suche nach einer Seite in DB {SECTOR_DB_ID}...")
try:
# 1. Wir holen uns die erste verfügbare Seite aus der Datenbank
response = notion.databases.query(
database_id=SECTOR_DB_ID,
page_size=1
)
results = response.get("results")
if not results:
print("⚠️ Keine Seiten in der Datenbank gefunden. Bitte lege manuell eine an.")
return
page = results[0]
print(f"✅ Seite gefunden: '{page['id']}'")
# 2. Wir inspizieren die Properties der Seite
properties = page.get("properties", {})
print("\n--- INTERNE PROPERTY-MAP DER SEITE ---")
print(json.dumps(properties, indent=2))
print("\n--- ZUSAMMENFASSUNG FÜR DEINE PIPELINE ---")
for prop_name, prop_data in properties.items():
print(f"Spaltenname: '{prop_name}' | ID: {prop_data.get('id')} | Typ: {prop_data.get('type')}")
except Exception as e:
print(f"💥 Fehler beim Inspect: {e}")
if __name__ == "__main__":
inspect_via_page()

View File

@@ -0,0 +1,68 @@
# create_feature_translator_db.py
import requests
import time
import json
# --- Configuration ---
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
exit(1)
PARENT_PAGE_ID = "2e088f42854480248289deb383da3818"
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# --- Database Schema ---
DB_NAME = "Feature-to-Value Translator"
DB_SCHEMA = {
"title": [{"type": "text", "text": {"content": DB_NAME}}],
"properties": {
"Feature": {"title": {}},
"Story (Benefit)": {"rich_text": {}},
"Headline": {"rich_text": {}},
"Product Master": {
"relation": {
"database_id": "2e288f42-8544-81d8-96f5-c231f84f719a", # Product Master DB ID
"dual_property": {}
}
}
}
}
# --- Main Logic ---
def main():
print(f"Attempting to create database: {DB_NAME}")
create_url = f"{NOTION_API_BASE_URL}/databases"
payload = {
"parent": {"type": "page_id", "page_id": PARENT_PAGE_ID},
"title": DB_SCHEMA["title"],
"properties": DB_SCHEMA["properties"],
}
try:
response = requests.post(create_url, headers=HEADERS, json=payload)
response.raise_for_status()
db_data = response.json()
db_id = db_data["id"]
print(f"Successfully created database '{DB_NAME}' with ID: {db_id}")
print("\n--- IMPORTANT ---")
print("Please update 'Notion_Dashboard.md' with this new ID.")
print(f"'Feature-to-Value Translator': '{db_id}'")
print("-------------------")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error creating database {DB_NAME}: {e}")
print(f"Response content: {response.text}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
import requests
import json
import os
TOKEN_FILE = 'notion_api_key.txt'
PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818" # "Roboplanet" page
def create_product_database(token):
print(f"Creating '📦 RoboPlanet Product Master' database under parent {PARENT_PAGE_ID}...")
url = "https://api.notion.com/v1/databases"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
database_definition = {
"parent": {"type": "page_id", "page_id": PARENT_PAGE_ID},
"title": [{"type": "text", "text": {"content": "📦 RoboPlanet Product Master"}}],
"properties": {
"Model Name": {"title": {}},
"Brand": {"select": {"options": [
{"name": "VIGGO", "color": "blue"},
{"name": "PUDU", "color": "orange"}
]}},
"Category": {"select": {"options": [
{"name": "cleaning", "color": "green"},
{"name": "service", "color": "blue"},
{"name": "security", "color": "red"}
]}},
# Core Specs
"Battery Runtime (min)": {"number": {"format": "number"}},
"Charge Time (min)": {"number": {"format": "number"}},
"Weight (kg)": {"number": {"format": "number"}},
"Max Slope (deg)": {"number": {"format": "number"}},
# Cleaning Layer
"Fresh Water (l)": {"number": {"format": "number"}},
"Area Performance (m2/h)": {"number": {"format": "number"}},
# Metadata
"Manufacturer URL": {"url": {}},
"GTM Status": {"status": {}}
}
}
try:
response = requests.post(url, headers=headers, json=database_definition)
response.raise_for_status()
new_db = response.json()
print(f"\n=== SUCCESS ===")
print(f"Database created! ID: {new_db['id']}")
print(f"URL: {new_db.get('url')}")
return new_db['id']
except requests.exceptions.HTTPError as e:
print(f"\n=== ERROR ===")
print(f"HTTP Error: {e}")
print(f"Response: {response.text}")
return None
def main():
try:
with open(TOKEN_FILE, 'r') as f:
token = f.read().strip()
except FileNotFoundError:
print(f"Error: Could not find '{TOKEN_FILE}'")
return
db_id = create_product_database(token)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,58 @@
import requests
import json
from getpass import getpass
def inspect_database_properties(db_id: str):
"""Liest die Eigenschaften (Spalten) einer Notion-Datenbank aus."""
print(f"--- Untersuche Eigenschaften von Notion DB: {db_id} ---")
token = getpass("Bitte gib deinen Notion API Key ein (Eingabe wird nicht angezeigt): ")
if not token:
print("\nFehler: Kein Token eingegeben. Abbruch.")
return
print(f"\n... Lese Struktur von Datenbank {db_id}...")
url = f"https://api.notion.com/v1/databases/{db_id}"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
database_info = response.json()
properties = database_info.get("properties", {})
print("\n✅ Erfolgreich! Folgende Spalten (Properties) wurden gefunden:")
print("--------------------------------------------------")
for name, details in properties.items():
prop_type = details.get("type")
print(f"Spaltenname: '{name}' (Typ: {prop_type})")
if prop_type == "relation":
relation_details = details.get("relation", {})
print(f" -> Verknüpft mit Datenbank-ID: {relation_details.get('database_id')}")
# Gib die verfügbaren Optionen für Status- und Select-Felder aus
elif prop_type in ["status", "select", "multi_select"]:
options = details.get(prop_type, {}).get("options", [])
if options:
print(f" -> Verfügbare Optionen:")
for option in options:
print(f" - '{option.get('name')}'")
print("--------------------------------------------------")
print("Bitte finde den korrekten Namen der Spalte, die zu den Projekten verknüpft ist, und den exakten Namen für den 'In Bearbeitung'-Status.")
except requests.exceptions.RequestException as e:
print(f"\n❌ FEHLER! Konnte die Datenbankstruktur nicht lesen: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"HTTP Status Code: {e.response.status_code}")
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
print(f"Antwort des Servers: {e.response.text}")
if __name__ == "__main__":
tasks_db_id = "2e888f42-8544-8153-beac-e604719029cf" # Die ID für "Tasks [UT]"
inspect_database_properties(tasks_db_id)

View File

@@ -0,0 +1,36 @@
import requests
import json
# Notion Config
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
exit(1)
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# DB ID from import_product.py
DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b"
def get_db_properties(database_id):
url = f"{NOTION_API_BASE_URL}/databases/{database_id}"
try:
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
return response.json().get("properties")
except Exception as e:
print(f"Error: {e}")
return None
props = get_db_properties(DB_ID)
if props:
print(json.dumps(props, indent=2))

View File

@@ -0,0 +1,63 @@
import requests
import json
from getpass import getpass
def debug_search_databases():
print("--- Notion Datenbank Such-Debugger ---")
token = getpass("Bitte gib deinen Notion API Key ein (Eingabe wird nicht angezeigt): ")
if not token:
print("\nFehler: Kein Token eingegeben. Abbruch.")
return
print("\n... Sende Suchanfrage an Notion für alle Datenbanken...")
url = "https://api.notion.com/v1/search"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28"
}
payload = {
"filter": {
"value": "database",
"property": "object"
},
"sort": {
"direction": "ascending",
"timestamp": "last_edited_time"
}
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status() # Hebt HTTPError für 4xx/5xx Statuscodes hervor
results = response.json().get("results", [])
if not results:
print("\nKeine Datenbanken gefunden, auf die die Integration Zugriff hat.")
print("Bitte stelle sicher, dass die Integration auf Top-Level-Seiten geteilt ist.")
return
print(f"\nGefundene Datenbanken ({len(results)} insgesamt):")
print("--------------------------------------------------")
for db in results:
db_id = db["id"]
db_title_parts = db.get("title", [])
db_title = db_title_parts[0].get("plain_text", "(Unbenannt)") if db_title_parts else "(Unbenannt)"
print(f"Titel: '{db_title}'\n ID: {db_id}\n")
print("--------------------------------------------------")
print("Bitte überprüfe die genauen Titel und IDs für 'Projects' und 'All Tasks'.")
except requests.exceptions.RequestException as e:
print(f"\n❌ FEHLER! Fehler bei der Suche nach Datenbanken: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"HTTP Status Code: {e.response.status_code}")
try:
print(f"Antwort des Servers: {json.dumps(e.response.json(), indent=2)}")
except:
print(f"Antwort des Servers: {e.response.text}")
if __name__ == "__main__":
debug_search_databases()

View File

@@ -0,0 +1,85 @@
import os
import json
import requests
from dotenv import load_dotenv
load_dotenv()
SESSION_FILE = ".dev_session/SESSION_INFO"
def debug_notion():
if not os.path.exists(SESSION_FILE):
print("No session file found.")
return
with open(SESSION_FILE, "r") as f:
data = json.load(f)
task_id = data.get("task_id")
token = data.get("token")
print(f"Debug Info:")
print(f"Task ID: {task_id}")
print(f"Token (first 4 chars): {token[:4]}...")
url = f"https://api.notion.com/v1/pages/{task_id}"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
# 1. Fetch Page
print("\n--- Fetching Page Properties ---")
resp = requests.get(url, headers=headers)
if resp.status_code != 200:
print(f"Error fetching page: {resp.status_code}")
print(resp.text)
return
page_data = resp.json()
properties = page_data.get("properties", {})
print(f"Found {len(properties)} properties:")
target_prop_name = "Total Duration (h)"
found_target = False
for name, prop in properties.items():
type_ = prop.get("type")
val = prop.get(type_)
print(f"- '{name}' ({type_}): {val}")
if name == target_prop_name:
found_target = True
if not found_target:
print(f"\nCRITICAL: Property '{target_prop_name}' NOT found on this task!")
# Check for similar names
for name in properties.keys():
if "duration" in name.lower() or "zeit" in name.lower() or "hours" in name.lower():
print(f" -> Did you mean: '{name}'?")
return
# 2. Try Update
print(f"\n--- Attempting Update of '{target_prop_name}' ---")
current_val = properties[target_prop_name].get("number") or 0.0
print(f"Current Value: {current_val}")
new_val = current_val + 0.01
print(f"Updating to: {new_val}")
update_payload = {
"properties": {
target_prop_name: {"number": new_val}
}
}
patch_resp = requests.patch(url, headers=headers, json=update_payload)
if patch_resp.status_code == 200:
print("✅ Update Successful!")
print(f"New Value on Server: {patch_resp.json()['properties'][target_prop_name].get('number')}")
else:
print(f"❌ Update Failed: {patch_resp.status_code}")
print(patch_resp.text)
if __name__ == "__main__":
debug_notion()

View File

@@ -0,0 +1,254 @@
# distribute_product_data.py
import requests
import json
import re
import os
import time
# --- Configuration ---
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
exit(1)
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# --- Database IDs (from Notion_Dashboard.md) ---
DB_IDS = {
"Product Master": "2e288f42-8544-81d8-96f5-c231f84f719a",
"Sector & Persona Master": "2e288f42-8544-8113-b878-ec99c8a02a6b",
"Messaging Matrix": "2e288f42-8544-81b0-83d4-c16623cc32d1",
"Feature-to-Value Translator": "2e288f42-8544-8184-ba08-d6d736879f19",
}
# --- Helper Functions ---
def create_notion_page(database_id, properties):
"""Creates a new page in a Notion database."""
url = f"{NOTION_API_BASE_URL}/pages"
payload = {"parent": {"database_id": database_id}, "properties": properties}
try:
response = requests.post(url, headers=HEADERS, json=payload)
response.raise_for_status()
print(f"Successfully created page in DB {database_id}.")
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error creating page in DB {database_id}: {e}\nResponse: {response.text}")
return None
def update_notion_page(page_id, properties):
"""Updates an existing page in Notion."""
url = f"{NOTION_API_BASE_URL}/pages/{page_id}"
payload = {"properties": properties}
try:
response = requests.patch(url, headers=HEADERS, json=payload)
response.raise_for_status()
print(f"Successfully updated page {page_id}.")
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error updating page {page_id}: {e}\nResponse: {response.text}")
return None
def find_notion_page_by_title(database_id, title):
"""Searches for a page in a Notion database by its title property."""
url = f"{NOTION_API_BASE_URL}/databases/{database_id}/query"
filter_payload = {"filter": {"property": "Name", "title": {"equals": title}}}
try:
response = requests.post(url, headers=HEADERS, json=filter_payload)
response.raise_for_status()
results = response.json().get("results")
return results[0] if results else None
except requests.exceptions.HTTPError as e:
print(f"HTTP Error searching page in DB {database_id}: {e}\nResponse: {response.text}")
return None
def get_page_property(page_id, property_id):
"""Retrieves a specific property from a Notion page."""
url = f"{NOTION_API_BASE_URL}/pages/{page_id}/properties/{property_id}"
try:
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error retrieving property {property_id}: {e}\nResponse: {response.text}")
return None
def get_rich_text_content(property_object):
"""Extracts plain text from a Notion rich_text property object."""
if not property_object:
return ""
try:
# The property endpoint returns a list in the 'results' key
if 'results' in property_object and property_object['results']:
# The actual content is in the 'rich_text' object within the first result
rich_text_items = property_object['results'][0].get('rich_text', {})
# It can be a single dict or a list, we handle the main plain_text for simplicity here
if isinstance(rich_text_items, dict) and 'plain_text' in rich_text_items:
return rich_text_items.get('plain_text', '')
# If it is a list of rich text objects (less common for a single property)
elif isinstance(rich_text_items, list):
return "".join(item.get("plain_text", "") for item in rich_text_items if isinstance(item, dict))
except (KeyError, IndexError, TypeError) as e:
print(f"Error parsing rich text object: {e}")
return ""
return ""
def format_rich_text(text):
"""Formats a string into Notion's rich text structure."""
if len(text) > 2000:
print(f"Warning: Truncating text from {len(text)} to 2000 characters.")
text = text[:2000]
return {"rich_text": [{"type": "text", "text": {"content": text}}]}
def format_title(text):
"""Formats a string into Notion's title structure."""
return {"title": [{"type": "text", "text": {"content": text}}]}
def format_relation(page_ids):
"""Formats a list of page IDs into Notion's relation structure."""
if not isinstance(page_ids, list):
page_ids = [page_ids]
return {"relation": [{"id": page_id} for page_id in page_ids]}
def parse_markdown_table(markdown_text):
"""Parses a generic markdown table into a list of dicts."""
lines = markdown_text.strip().split('\n')
if len(lines) < 2:
return []
headers = [h.strip() for h in lines[0].split('|') if h.strip()]
data_rows = []
for line in lines[2:]: # Skip header and separator
values = [v.strip() for v in line.split('|') if v.strip()]
if len(values) == len(headers):
data_rows.append(dict(zip(headers, values)))
return data_rows
# --- Main Logic ---
def main():
PRODUCT_NAME = "Puma M20"
print(f"--- Starting data distribution for product: {PRODUCT_NAME} ---")
# 1. Get the product page from Product Master
product_page = find_notion_page_by_title(DB_IDS["Product Master"], PRODUCT_NAME)
if not product_page:
print(f"Product '{PRODUCT_NAME}' not found. Aborting.")
return
product_page_id = product_page["id"]
print(f"Found Product Page ID: {product_page_id}")
# 2. Distribute Strategy Matrix Data
strategy_matrix_prop_id = product_page["properties"]["Strategy Matrix"]["id"]
strategy_matrix_obj = get_page_property(product_page_id, strategy_matrix_prop_id)
strategy_matrix_text = get_rich_text_content(strategy_matrix_obj)
if strategy_matrix_text:
parsed_matrix = parse_markdown_table(strategy_matrix_text)
if parsed_matrix:
print("\n--- Distributing Strategy Matrix Data ---")
sector_page_ids_for_product = []
for row in parsed_matrix:
segment_name = row.get("Segment")
pain_point = row.get("Pain Point")
angle = row.get("Angle")
differentiation = row.get("Differentiation")
if not all([segment_name, pain_point, angle, differentiation]):
print(f"Skipping row due to missing data: {row}")
continue
print(f"\nProcessing Segment: {segment_name}")
# Find or Create Sector in Sector & Persona Master
sector_page = find_notion_page_by_title(DB_IDS["Sector & Persona Master"], segment_name)
if sector_page:
sector_page_id = sector_page["id"]
print(f"Found existing Sector page with ID: {sector_page_id}")
update_notion_page(sector_page_id, {"Pains": format_rich_text(pain_point)})
else:
print(f"Creating new Sector page for '{segment_name}'...")
new_sector_page = create_notion_page(DB_IDS["Sector & Persona Master"], {"Name": format_title(segment_name), "Pains": format_rich_text(pain_point)})
if not new_sector_page:
print(f"Failed to create sector page for '{segment_name}'. Skipping.")
continue
sector_page_id = new_sector_page["id"]
sector_page_ids_for_product.append(sector_page_id)
# Create entry in Messaging Matrix
print(f"Creating Messaging Matrix entry for '{segment_name}'...")
messaging_properties = {
"Name": format_title(f"{PRODUCT_NAME} - {segment_name}"),
"Satz 1": format_rich_text(angle),
"Satz 2": format_rich_text(differentiation),
"Product Master": format_relation(product_page_id),
"Sector Master": format_relation(sector_page_id)
}
create_notion_page(DB_IDS["Messaging Matrix"], messaging_properties)
# Update Product Master with relations to all processed sectors
if sector_page_ids_for_product:
print(f"\nUpdating Product Master with relations to {len(sector_page_ids_for_product)} sectors...")
update_notion_page(product_page_id, {"Sector Master": format_relation(sector_page_ids_for_product)})
# Clean up redundant fields in Product Master
print("Cleaning up redundant Strategy Matrix field in Product Master...")
update_notion_page(product_page_id, {"Strategy Matrix": format_rich_text("")})
else:
print("Strategy Matrix is empty. Skipping distribution.")
# 3. Distribute Feature-to-Value Translator Data
feature_translator_prop_id = product_page["properties"]["Feature-to-Value Translator"]["id"]
feature_translator_obj = get_page_property(product_page_id, feature_translator_prop_id)
feature_translator_text = get_rich_text_content(feature_translator_obj)
if feature_translator_text:
parsed_features = parse_markdown_table(feature_translator_text)
if parsed_features:
print("\n--- Distributing Feature-to-Value Translator Data ---")
for item in parsed_features:
feature = item.get("Feature")
story = item.get("The Story (Benefit)")
headline = item.get("Headline")
if not all([feature, story, headline]):
print(f"Skipping feature item due to missing data: {item}")
continue
print(f"Creating Feature-to-Value entry for: {feature}")
create_notion_page(
DB_IDS["Feature-to-Value Translator"],
{
"Feature": format_title(feature),
"Story (Benefit)": format_rich_text(story),
"Headline": format_rich_text(headline),
"Product Master": format_relation(product_page_id)
}
)
# Clean up the source field
print("Cleaning up redundant Feature-to-Value Translator field in Product Master...")
update_notion_page(product_page_id, {"Feature-to-Value Translator": format_rich_text("")})
else:
print("Feature-to-Value Translator is empty. Skipping distribution.")
print("\n--- Data distribution process complete. ---")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,72 @@
import requests
import json
import os
TOKEN_FILE = 'notion_api_key.txt'
PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818" # "Roboplanet" page
def main():
try:
with open(TOKEN_FILE, 'r') as f:
token = f.read().strip()
except FileNotFoundError:
print(f"Error: Could not find '{TOKEN_FILE}'")
return
print(f"Creating 'Hello World' page under parent {PARENT_PAGE_ID}...")
url = "https://api.notion.com/v1/pages"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
payload = {
"parent": { "page_id": PARENT_PAGE_ID },
"properties": {
"title": [
{
"text": {
"content": "Hello World"
}
}
]
},
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "This page was created automatically by the GTM Engine Bot."
}
}
]
}
}
]
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
print("\n=== SUCCESS ===")
print(f"New page created!")
print(f"URL: {data.get('url')}")
except requests.exceptions.HTTPError as e:
print(f"\n=== ERROR ===")
print(f"HTTP Error: {e}")
print(f"Response: {response.text}")
except Exception as e:
print(f"\n=== ERROR ===")
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,179 @@
import json
import requests
import sys
# --- CONFIGURATION ---
JSON_FILE = 'analysis_robo-planet.de-4.json'
NOTION_TOKEN = "" # Will be loaded from file
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
}
# --- DATABASE IDs ---
COMPANIES_DB_ID = "2e688f42-8544-8158-8673-d8b1e3eca5b5"
CANONICAL_PRODUCTS_DB_ID = "2f088f42-8544-81d5-bec7-d9189f3bacd4"
PORTFOLIO_DB_ID = "2e688f42-8544-81df-8fcc-f1d7f8745e00"
LANDMINES_DB_ID = "" # Optional: Add if you want to re-import landmines
REFERENCES_DB_ID = "" # Optional: Add if you want to re-import references
# --- API HELPERS ---
def query_db(db_id, filter_payload=None):
"""Retrieves all pages from a Notion database, with optional filter."""
url = f"https://api.notion.com/v1/databases/{db_id}/query"
all_pages = []
start_cursor = None
while True:
payload = {}
if start_cursor:
payload["start_cursor"] = start_cursor
if filter_payload:
payload["filter"] = filter_payload
response = requests.post(url, headers=HEADERS, json=payload)
if response.status_code != 200:
print(f"Error querying DB {db_id}: {response.status_code}")
print(response.json())
return None
data = response.json()
all_pages.extend(data["results"])
if data.get("has_more"):
start_cursor = data["next_cursor"]
else:
break
return all_pages
def create_page(db_id, properties):
"""Creates a new page in a Notion database."""
url = "https://api.notion.com/v1/pages"
payload = {"parent": {"database_id": db_id}, "properties": properties}
response = requests.post(url, headers=HEADERS, data=json.dumps(payload))
if response.status_code == 200:
return response.json()
else:
print(f"Error creating page in DB {db_id}: {response.status_code}")
print(response.json())
return None
# --- STATE AWARENESS HELPERS ---
def get_existing_items_map(db_id, name_property="Name"):
"""Fetches all items from a DB and returns a map of {name: id}."""
print(f"Fetching existing items from DB {db_id} to build cache...")
pages = query_db(db_id)
if pages is None:
sys.exit(f"Could not fetch items from DB {db_id}. Aborting.")
item_map = {}
for page in pages:
try:
item_name = page["properties"][name_property]["title"][0]["text"]["content"]
item_map[item_name] = page["id"]
except (KeyError, IndexError):
continue
print(f" - Found {len(item_map)} existing items.")
return item_map
def get_existing_portfolio_links(db_id):
"""Fetches all portfolio links and returns a set of (company_id, product_id) tuples."""
print(f"Fetching existing portfolio links from DB {db_id}...")
pages = query_db(db_id)
if pages is None:
sys.exit(f"Could not fetch portfolio links from DB {db_id}. Aborting.")
link_set = set()
for page in pages:
try:
company_id = page["properties"]["Related Competitor"]["relation"][0]["id"]
product_id = page["properties"]["Canonical Product"]["relation"][0]["id"]
link_set.add((company_id, product_id))
except (KeyError, IndexError):
continue
print(f" - Found {len(link_set)} existing portfolio links.")
return link_set
# --- MAIN LOGIC ---
def main():
global NOTION_TOKEN, HEADERS
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
HEADERS["Authorization"] = f"Bearer {NOTION_TOKEN}"
except FileNotFoundError:
print("Error: `notion_token.txt` not found.")
return
# --- Phase 1: State Awareness ---
print("\n--- Phase 1: Reading current state from Notion ---")
companies_map = get_existing_items_map(COMPANIES_DB_ID)
products_map = get_existing_items_map(CANONICAL_PRODUCTS_DB_ID)
portfolio_links = get_existing_portfolio_links(PORTFOLIO_DB_ID)
# --- Phase 2: Processing JSON ---
print("\n--- Phase 2: Processing local JSON file ---")
try:
with open(JSON_FILE, 'r') as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: `{JSON_FILE}` not found.")
return
for analysis in data.get('analyses', []):
competitor = analysis['competitor']
competitor_name = competitor['name']
print(f"\nProcessing competitor: {competitor_name}")
# --- Phase 3: "Upsert" Company ---
if competitor_name not in companies_map:
print(f" - Company '{competitor_name}' not found. Creating...")
props = {"Name": {"title": [{"text": {"content": competitor_name}}]}}
new_company = create_page(COMPANIES_DB_ID, props)
if new_company:
companies_map[competitor_name] = new_company["id"]
else:
print(f" - Failed to create company '{competitor_name}'. Skipping.")
continue
company_id = companies_map[competitor_name]
# --- Phase 4: "Upsert" Products and Portfolio Links ---
for product in analysis.get('portfolio', []):
product_name = product['product']
# Upsert Canonical Product
if product_name not in products_map:
print(f" - Product '{product_name}' not found. Creating canonical product...")
props = {"Name": {"title": [{"text": {"content": product_name}}]}}
new_product = create_page(CANONICAL_PRODUCTS_DB_ID, props)
if new_product:
products_map[product_name] = new_product["id"]
else:
print(f" - Failed to create canonical product '{product_name}'. Skipping.")
continue
product_id = products_map[product_name]
# Check and create Portfolio Link
if (company_id, product_id) not in portfolio_links:
print(f" - Portfolio link for '{competitor_name}' -> '{product_name}' not found. Creating...")
portfolio_props = {
"Product": {"title": [{"text": {"content": f"{competitor_name} - {product_name}"}}]},
"Related Competitor": {"relation": [{"id": company_id}]},
"Canonical Product": {"relation": [{"id": product_id}]}
}
new_portfolio_entry = create_page(PORTFOLIO_DB_ID, portfolio_props)
if new_portfolio_entry:
portfolio_links.add((company_id, product_id)) # Add to cache to prevent re-creation in same run
else:
print(f" - Portfolio link for '{competitor_name}' -> '{product_name}' already exists. Skipping.")
print("\n--- ✅ Import script finished ---")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,263 @@
import requests
import json
import re
import os
import time
# --- Configuration ---
# NOTION_TOKEN wird jetzt aus der Datei gelesen
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
print("Please create the notion_token.txt file with your Notion integration token.")
exit(1)
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# --- Database IDs (from Notion_Dashboard.md) ---
DB_IDS = {
"Product Master": "2e288f42-8544-81d8-96f5-c231f84f719a",
"Sector & Persona Master": "2e288f42-8544-8113-b878-ec99c8a02a6b",
"Messaging Matrix": "2e288f42-8544-81b0-83d4-c16623cc32d1",
}
# --- Helper Functions ---
def clean_json_response(text):
if text.startswith("```json") and text.endswith("```"):
return text[7:-3].strip()
return text
def create_notion_page(database_id, properties):
"""Creates a new page in a Notion database."""
url = f"{NOTION_API_BASE_URL}/pages"
payload = {
"parent": {"database_id": database_id},
"properties": properties,
}
try:
response = requests.post(url, headers=HEADERS, json=payload)
response.raise_for_status()
print(f"Successfully created page in DB {database_id}.")
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error creating page in DB {database_id}: {e}")
print(f"Response content: {response.text}")
return None
except Exception as e:
print(f"An unexpected error occurred while creating a page: {e}")
return None
def update_notion_page(page_id, properties):
"""Updates an existing page in Notion."""
url = f"{NOTION_API_BASE_URL}/pages/{page_id}"
payload = {
"properties": properties
}
try:
response = requests.patch(url, headers=HEADERS, json=payload)
response.raise_for_status()
print(f"Successfully updated page {page_id}.")
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error updating page {page_id}: {e}")
print(f"Response content: {response.text}")
return None
except Exception as e:
print(f"An unexpected error occurred while updating a page: {e}")
return None
def find_notion_page_by_title(database_id, title):
"""Searches for a page in a Notion database by its title property."""
url = f"{NOTION_API_BASE_URL}/databases/{database_id}/query"
filter_payload = {
"filter": {
"property": "Name",
"title": {"equals": title}
}
}
try:
response = requests.post(url, headers=HEADERS, json=filter_payload)
response.raise_for_status()
results = response.json().get("results")
if results:
return results[0] # Return the first matching page
return None
except requests.exceptions.HTTPError as e:
print(f"HTTP Error searching page in DB {database_id}: {e}")
print(f"Response content: {response.text}")
return None
except Exception as e:
print(f"An unexpected error occurred while searching for a page: {e}")
return None
def get_database_properties(database_id):
"""Retrieves the properties (schema) of a Notion database."""
url = f"{NOTION_API_BASE_URL}/databases/{database_id}"
try:
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
return response.json().get("properties")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error retrieving database properties for DB {database_id}: {e}")
print(f"Response content: {response.text}")
return None
except Exception as e:
print(f"An unexpected error occurred while retrieving database properties: {e}")
return None
def format_rich_text(text):
"""Formats a string into Notion's rich text structure."""
return {"rich_text": [{"type": "text", "text": {"content": text}}]}
def format_title(text):
"""Formats a string into Notion's title structure."""
return {"title": [{"type": "text", "text": {"content": text}}]}
def format_relation(page_ids):
"""Formats a list of page IDs into Notion's relation structure."""
if not isinstance(page_ids, list):
page_ids = [page_ids] # Ensure it's a list
return {"relation": [{"id": page_id} for page_id in page_ids]}
def extract_section(content, title):
"""Extracts a section from markdown content based on a ## title."""
pattern = re.compile(rf"## {re.escape(title)}\n(.*?)(?=\n## |\Z)", re.S)
match = pattern.search(content)
return match.group(1).strip() if match else ""
# --- Main Import Logic ---
def main():
if NOTION_TOKEN == "YOUR_NOTION_TOKEN":
print("ERROR: Please replace 'YOUR_NOTION_TOKEN' in the script with your actual Notion token.")
return
# 1. Read the markdown file
try:
with open("Puma_m20_2026-01-08.md", "r", encoding="utf-8") as f:
md_content = f.read()
except FileNotFoundError:
print("ERROR: 'Puma_m20_2026-01-08.md' not found. Please make sure the file is in the same directory.")
return
# Define the product name
PRODUCT_NAME = "Puma M20" # This will be replaced by the user's actual product name.
# --- Phase 1: Prepare Product Data ---
print(f"--- Phase 1: Preparing Product Data for {PRODUCT_NAME} ---")
product_analysis = extract_section(md_content, "2. Product Analysis")
key_features = re.search(r"\*\*Key Features:\*\*(.*?)\*\*Constraints:\*\*", product_analysis, re.S).group(1).strip()
constraints = re.search(r"\*\*Constraints:\*\*(.*)", product_analysis, re.S).group(1).strip()
target_audience = extract_section(md_content, "3. Target Audience")
strategy_matrix = extract_section(md_content, "5. Strategy Matrix")
if len(strategy_matrix) > 2000:
strategy_matrix = strategy_matrix[:2000] # Truncate to 2000 characters
print("Warning: 'Strategy Matrix' content truncated to 2000 characters due to Notion API limit.")
feature_translator = extract_section(md_content, "FEATURE-TO-VALUE TRANSLATOR (PHASE 9)")
product_properties = {
"Name": format_title(PRODUCT_NAME),
"Beschreibung": format_rich_text("Ein geländegängiger, wetterfester Roboter, der für anspruchsvolle Umgebungen konzipiert wurde."),
"Key Features": format_rich_text(key_features),
"Constraints": format_rich_text(constraints),
"Target Audience": format_rich_text(target_audience),
"Strategy Matrix": format_rich_text(strategy_matrix),
"Feature-to-Value Translator": format_rich_text(feature_translator),
"Layer": {"multi_select": [{"name": "Security"}, {"name": "Service"}]}
}
# Check if product already exists
existing_product_page = find_notion_page_by_title(DB_IDS["Product Master"], PRODUCT_NAME)
product_page_id = None
if existing_product_page:
product_page_id = existing_product_page["id"]
print(f"Product '{PRODUCT_NAME}' already exists with ID: {product_page_id}. Updating...")
updated_page = update_notion_page(product_page_id, product_properties)
if not updated_page:
print("Failed to update product page. Aborting.")
return
else:
print(f"Product '{PRODUCT_NAME}' not found. Creating new page...")
new_product_page = create_notion_page(DB_IDS["Product Master"], product_properties)
if not new_product_page:
print("Failed to create product page. Aborting.")
return
product_page_id = new_product_page["id"]
print(f"Created Product '{PRODUCT_NAME}' with ID: {product_page_id}")
# --- Phase 2: Create Sectors in Sector & Persona Master ---
print("\n--- Phase 2: Creating Sectors ---")
sector_pages = {}
sectors = {
"Chemieparks/Petrochemische Anlagen": {
"definition": "Anlagen dieser Art haben ausgedehnte Gelände, komplexe Infrastruktur und hohe Sicherheitsanforderungen...",
"pains": "Umfangreiche Gelände erfordern ständige Sicherheits- und Inspektionsrundgänge, oft unter gefährlichen Bedingungen. Personalmangel und hohe Kosten für manuelle Inspektionen.",
"personas": ["Head of Security", "Werkschutzleiter", "Geschäftsführer/Vorstand", "Leiter Instandhaltung / Betriebsleiter"]
},
"Energieversorgungsunternehmen (z.B. Windparks, Solarparks)": {
"definition": "Diese Anlagen erstrecken sich oft über große, schwer zugängliche Gebiete...",
"pains": "Weitläufige Anlagen in oft unwegsamem Gelände. Schwierige und teure Inspektion von Solarmodulen oder Windkraftanlagen. Anfälligkeit für Vandalismus und Diebstahl.",
"personas": ["Head of Security", "Geschäftsführer/Vorstand", "Leiter Instandhaltung / Betriebsleiter"]
},
"Logistikzentren/Großflächenlager": {
"definition": "Große Lagerflächen und komplexe Logistikprozesse erfordern eine ständige Überwachung und Inspektion.",
"pains": "Hohe Anforderungen an Sicherheit und Ordnung in großen Lagerhallen... Ineffiziente manuelle Reinigung großer Flächen. Gefahr von Unfällen...",
"personas": ["Leiter Instandhaltung / Betriebsleiter", "Geschäftsführer/Vorstand", "Head of Security"]
}
}
for name, data in sectors.items():
sector_properties = {
"Name": format_title(name),
"RoboPlanet-Definition": format_rich_text(data["definition"]),
"Pains": format_rich_text(data["pains"]),
"Personas": {"multi_select": [{"name": p} for p in data["personas"]]}
}
sector_page = create_notion_page(DB_IDS["Sector & Persona Master"], sector_properties)
if sector_page:
sector_pages[name] = sector_page["id"]
print(f"Created Sector '{name}' with ID: {sector_page['id']}")
else:
print(f"Failed to create sector '{name}'.")
# --- Phase 3: Create Messaging Elements ---
print("\n--- Phase 3: Creating Messaging Elements (Battlecards) ---")
battlecards_content = extract_section(md_content, "Kill-Critique Battlecards")
battlecards = re.findall(r"### Persona: (.*?)\n> \*\*Objection:\*\* \"(.*?)\"\n\n\*\*Response:\*\* (.*?)(?=\n\n---|\Z)", battlecards_content, re.S)
for persona, objection, response in battlecards:
# Determine which sector this battlecard applies to
current_sector_id = None
if "Chemiepark" in response or "Wackler Security" in response:
current_sector_id = sector_pages.get("Chemieparks/Petrochemische Anlagen")
if "Logistik" in response or "Reinigung" in response:
current_sector_id = sector_pages.get("Logistikzentren/Großflächenlager")
message_properties = {
"Name": format_title(f"Objection: {objection}"),
"Satz 1": format_rich_text(f"Persona: {persona.strip()}\nObjection: {objection}"),
"Satz 2": format_rich_text(response.strip()),
"Product Master": format_relation(product_page_id),
}
if current_sector_id:
message_properties["Sector Master"] = format_relation(current_sector_id)
create_notion_page(DB_IDS["Messaging Matrix"], message_properties)
print("\nImport process complete.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,320 @@
import json
import requests
import sys
import argparse
import re
# --- CONFIGURATION ---
NOTION_TOKEN = "" # Will be loaded from file
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
}
# --- DATABASE IDs ---
COMPANIES_DB_ID = "2e688f42-8544-8158-8673-d8b1e3eca5b5"
CANONICAL_PRODUCTS_DB_ID = "2f088f42-8544-81d5-bec7-d9189f3bacd4"
PORTFOLIO_DB_ID = "2e688f42-8544-81df-8fcc-f1d7f8745e00"
LANDMINES_DB_ID = "2e688f42-8544-81aa-94f8-d6242be4d0cd"
REFERENCES_DB_ID = "2e688f42-8544-81df-8d83-f4d7f57d8168"
INDUSTRIES_DB_ID = "2ec88f42-8544-8014-ab38-ea664b4c2b81"
# --- API HELPERS ---
def query_db(db_id, filter_payload=None):
"""Retrieves all pages from a Notion database, with optional filter."""
url = f"https://api.notion.com/v1/databases/{db_id}/query"
all_pages = []
start_cursor = None
while True:
payload = {}
if start_cursor:
payload["start_cursor"] = start_cursor
if filter_payload:
payload["filter"] = filter_payload
response = requests.post(url, headers=HEADERS, json=payload)
if response.status_code != 200:
print(f"Error querying DB {db_id}: {response.status_code}")
print(response.json())
return None
data = response.json()
all_pages.extend(data["results"])
if data.get("has_more"):
start_cursor = data["next_cursor"]
else:
break
return all_pages
def create_page(db_id, properties):
"""Creates a new page in a Notion database."""
url = "https://api.notion.com/v1/pages"
payload = {"parent": {"database_id": db_id}, "properties": properties}
response = requests.post(url, headers=HEADERS, data=json.dumps(payload))
if response.status_code == 200:
return response.json()
else:
print(f"Error creating page in DB {db_id}: {response.status_code}")
print(response.json())
return None
def update_page(page_id, properties):
"""Updates properties of an existing page in Notion."""
url = f"https://api.notion.com/v1/pages/{page_id}"
payload = {"properties": properties}
response = requests.patch(url, headers=HEADERS, data=json.dumps(payload))
if response.status_code == 200:
return response.json()
else:
print(f"Error updating page {page_id}: {response.status_code}")
print(response.json())
return None
# --- STATE AWARENESS HELPERS ---
def get_existing_items_map(db_id, name_property="Name"):
"""Fetches all items from a DB and returns a map of {name: id}."""
print(f"Fetching existing items from DB {db_id} to build cache...")
pages = query_db(db_id)
if pages is None:
sys.exit(f"Could not fetch items from DB {db_id}. Aborting.")
item_map = {}
for page in pages:
try:
# Handle cases where title might be empty or malformed
title_list = page["properties"][name_property].get("title", [])
if title_list:
item_name = title_list[0].get("text", {}).get("content", "").strip()
if item_name:
item_map[item_name] = page["id"]
except (KeyError, IndexError):
continue
print(f" - Found {len(item_map)} existing items.")
return item_map
def get_existing_relations(db_id, relation_property_name, target_relation_id_prop_name):
"""Fetches all items from a DB and returns a set of (item_name, related_id) tuples."""
print(f"Fetching existing relations from DB {db_id}...")
pages = query_db(db_id)
if pages is None:
sys.exit(f"Could not fetch relations from DB {db_id}. Aborting.")
relation_set = set()
for page in pages:
try:
item_name = page["properties"]["Name"]["title"][0]["text"]["content"]
related_ids = [rel["id"] for rel in page["properties"][relation_property_name].get("relation", [])]
target_related_ids = [rel["id"] for rel in page["properties"][target_relation_id_prop_name].get("relation", [])]
if related_ids and target_related_ids:
relation_set.add((item_name, related_ids[0], target_related_ids[0]))
except (KeyError, IndexError):
continue
print(f" - Found {len(relation_set)} existing relations.")
return relation_set
def inspect_database(db_id):
"""Retrieves and prints the properties of a specific Notion database."""
print(f"🔍 Inspecting properties for database ID: {db_id}")
url = f"https://api.notion.com/v1/databases/{db_id}"
response = requests.get(url, headers=HEADERS)
if response.status_code != 200:
print(f"Error retrieving database properties: {response.status_code}")
print(response.json())
return
data = response.json()
properties = data.get("properties", {})
if not properties:
print("No properties found for this database.")
return
print("\n--- Database Properties ---")
for prop_name, prop_data in properties.items():
print(f"- Property Name: '{prop_name}'")
print(f" Type: {prop_data.get('type')}\n")
print("---------------------------\n")
# --- MAIN LOGIC ---
def main():
global NOTION_TOKEN, HEADERS
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
HEADERS["Authorization"] = f"Bearer {NOTION_TOKEN}"
except FileNotFoundError:
print("Error: `notion_token.txt` not found.")
return
parser = argparse.ArgumentParser(description="Import a single competitor from a JSON analysis file into Notion.")
parser.add_argument('--file', help="Path to the JSON analysis file.")
parser.add_argument('--name', help="Exact name of the competitor to import.")
parser.add_argument('--inspect', help="Database ID to inspect.")
args = parser.parse_args()
if args.inspect:
inspect_database(args.inspect)
return
if not args.file or not args.name:
parser.error("--file and --name are required.")
return
# --- Phase 1: State Awareness ---
print("\n--- Phase 1: Reading current state from Notion ---")
companies_map = get_existing_items_map(COMPANIES_DB_ID)
products_map = get_existing_items_map(CANONICAL_PRODUCTS_DB_ID)
industries_map = get_existing_items_map(INDUSTRIES_DB_ID, name_property="Vertical")
# For relations, we create a unique key to check for existence
existing_landmines = {f'{page["properties"]["Question"]["title"][0]["text"]["content"]}_{page["properties"]["Related Competitor"]["relation"][0]["id"]}' for page in query_db(LANDMINES_DB_ID) if "Question" in page["properties"] and page["properties"]["Question"]["title"] and page["properties"]["Related Competitor"]["relation"]}
print(f" - Found {len(existing_landmines)} existing landmines.")
existing_references = {f'{page["properties"]["Customer"]["title"][0]["text"]["content"]}_{page["properties"]["Related Competitor"]["relation"][0]["id"]}' for page in query_db(REFERENCES_DB_ID) if "Customer" in page["properties"] and page["properties"]["Customer"]["title"] and page["properties"]["Related Competitor"]["relation"]}
print(f" - Found {len(existing_references)} existing references.")
json_file_path = args.file
target_competitor_name = args.name
# --- Phase 2: Processing JSON ---
print(f"\n--- Phase 2: Processing local JSON file: {json_file_path} for {target_competitor_name} ---")
try:
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: `{json_file_path}` not found.")
return
except json.JSONDecodeError as e:
print(f"Error decoding JSON from {json_file_path}: {e}")
return
# Find the correct analysis and reference data for the target competitor
target_analysis = None
for analysis in data.get('analyses', []):
if analysis['competitor']['name'] == target_competitor_name:
target_analysis = analysis
break
# Find references from the separate reference_analysis block
target_references_data = None
if 'reference_analysis' in data:
for ref_block in data.get('reference_analysis', []):
if ref_block.get('competitor_name') == target_competitor_name:
target_references_data = ref_block.get('references', [])
break
target_battlecard = None
if 'battlecards' in data:
for bc in data.get('battlecards', []):
if bc['competitor_name'] == target_competitor_name:
target_battlecard = bc
break
if not target_analysis:
print(f"Error: Competitor '{target_competitor_name}' not found in 'analyses' list in {json_file_path}.")
return
print(f"\nProcessing target competitor: {target_competitor_name}")
# --- Phase 3: "Upsert" Company ---
if target_competitor_name not in companies_map:
print(f" - Company '{target_competitor_name}' not found. Creating...")
props = {"Name": {"title": [{"text": {"content": target_competitor_name}}]}}
new_company = create_page(COMPANIES_DB_ID, props)
if new_company:
companies_map[target_competitor_name] = new_company["id"]
else:
print(f" - Failed to create company '{target_competitor_name}'. Halting.")
return
company_id = companies_map[target_competitor_name]
# --- Phase 4: Create and Link Target Industries ---
print("\n--- Processing Target Industries ---")
target_industry_relation_ids = []
if INDUSTRIES_DB_ID:
for industry_name in target_analysis.get('target_industries', []):
if industry_name not in industries_map:
print(f" - Industry '{industry_name}' not found in Notion DB. Creating...")
props = {"Vertical": {"title": [{"text": {"content": industry_name}}]}}
new_industry = create_page(INDUSTRIES_DB_ID, props)
if new_industry:
industries_map[industry_name] = new_industry["id"]
target_industry_relation_ids.append({"id": new_industry["id"]})
else:
target_industry_relation_ids.append({"id": industries_map[industry_name]})
if target_industry_relation_ids:
print(f" - Linking company to {len(target_analysis.get('target_industries', []))} industries...")
# Format for multi-select is a list of objects with names
multi_select_payload = [{"name": name} for name in target_analysis.get('target_industries', [])]
update_props = {
"Target Industries": {"multi_select": multi_select_payload}
}
update_page(company_id, update_props)
else:
print(" - INDUSTRIES_DB_ID not set. Skipping.")
# --- Phase 5: Import Landmines ---
if target_battlecard and LANDMINES_DB_ID:
print("\n--- Processing Landmines ---")
for landmine in target_battlecard.get('landmine_questions', []):
unique_key = f"{landmine}_{company_id}"
if unique_key not in existing_landmines:
print(f" - Landmine '{landmine}' not found. Creating...")
props = {
"Question": {"title": [{"text": {"content": landmine}}]},
"Related Competitor": {"relation": [{"id": company_id}]}
}
new_landmine = create_page(LANDMINES_DB_ID, props)
if new_landmine:
existing_landmines.add(unique_key)
else:
print(f" - Landmine '{landmine}' already exists for this competitor. Skipping.")
# --- Phase 6: Import References ---
if target_references_data and REFERENCES_DB_ID:
print("\n--- Processing References ---")
for ref in target_references_data:
ref_name = ref.get("name", "Unknown Reference")
unique_key = f"{ref_name}_{company_id}"
if unique_key not in existing_references:
print(f" - Reference '{ref_name}' not found. Creating...")
props = {
"Customer": {"title": [{"text": {"content": ref_name}}]},
"Related Competitor": {"relation": [{"id": company_id}]},
"Quote": {"rich_text": [{"text": {"content": ref.get("testimonial_snippet", "")[:2000]}}]}
}
# Handle Industry as a select property
ref_industry_name = ref.get("industry")
if ref_industry_name:
props["Industry"] = {"select": {"name": ref_industry_name}}
new_ref = create_page(REFERENCES_DB_ID, props)
if new_ref:
existing_references.add(unique_key)
else:
print(f" - Reference '{ref_name}' already exists for this competitor. Skipping.")
print("\n--- ✅ Import script finished ---")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,24 @@
import sys
import os
import requests
import json
NOTION_TOKEN_FILE = "/app/notion_token.txt"
PERSONAS_DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b"
def load_notion_token():
with open(NOTION_TOKEN_FILE, "r") as f:
return f.read().strip()
def query_notion_db(token, db_id):
url = f"https://api.notion.com/v1/databases/{db_id}/query"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28"
}
response = requests.post(url, headers=headers)
return response.json()
token = load_notion_token()
data = query_notion_db(token, PERSONAS_DB_ID)
print(json.dumps(data.get("results", [])[0], indent=2))

View File

@@ -0,0 +1,30 @@
import sys
import os
import requests
import json
NOTION_TOKEN_FILE = "/app/notion_token.txt"
PERSONAS_DB_ID = "30588f42-8544-80c3-8919-e22d74d945ea"
def load_notion_token():
with open(NOTION_TOKEN_FILE, "r") as f:
return f.read().strip()
def query_notion_db(token, db_id):
url = f"https://api.notion.com/v1/databases/{db_id}/query"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28"
}
response = requests.post(url, headers=headers)
return response.json()
token = load_notion_token()
data = query_notion_db(token, PERSONAS_DB_ID)
results = data.get("results", [])
for res in results:
props = res.get("properties", {})
role = "".join([t.get("plain_text", "") for t in props.get("Role", {}).get("title", [])])
print(f"Role: {role}")
print(json.dumps(props, indent=2))
print("-" * 40)

View File

@@ -0,0 +1,220 @@
import requests
import time
import json
# --- Configuration ---
NOTION_TOKEN = "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" # Replace with your actual Notion integration token
PARENT_PAGE_ID = "2e088f42854480248289deb383da3818" # Replace with the ID of the Notion page where you want to create the databases
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# --- Database Schemas ---
# Define basic properties for each database. Relations will be added in a second phase.
DATABASE_SCHEMAS = {
"Product Master": {
"title": [{"type": "text", "text": {"content": "Product Master"}}],
"properties": {
"Name": {"title": {}},
"Beschreibung": {"rich_text": {}},
"Spezifikationen": {"rich_text": {}},
"Layer": {"multi_select": {"options": [{"name": "Cleaning"}, {"name": "Service"}, {"name": "Security"}]}},
}
},
"Sector & Persona Master": {
"title": [{"type": "text", "text": {"content": "Sector & Persona Master"}}],
"properties": {
"Name": {"title": {}},
"RoboPlanet-Definition": {"rich_text": {}},
"Personas": {"multi_select": {"options": []}}, # Options can be added later if known
"Pains": {"rich_text": {}},
"Gains": {"rich_text": {}},
"Probing Questions": {"rich_text": {}},
}
},
"Messaging Matrix": {
"title": [{"type": "text", "text": {"content": "Messaging Matrix"}}],
"properties": {
"Name": {"title": {}},
"Satz 1": {"rich_text": {}},
"Satz 2": {"rich_text": {}},
"Voice Script": {"rich_text": {}},
}
},
"Competitive Radar": {
"title": [{"type": "text", "text": {"content": "Competitive Radar"}}],
"properties": {
"Wettbewerber": {"title": {}},
"News": {"url": {}},
"Blogposts": {"url": {}},
"Kill-Argumente": {"rich_text": {}},
"Technische Specs": {"rich_text": {}},
}
},
"Enrichment Factory & RevOps": {
"title": [{"type": "text", "text": {"content": "Enrichment Factory & RevOps"}}],
"properties": {
"Account Name": {"title": {}},
"Umsatz": {"number": {"format": "euro"}},
"Mitarbeiter": {"number": {"format": "number"}},
"Ansprechpartner": {"rich_text": {}},
"Job Titel": {"rich_text": {}},
"Klassifizierung": {"multi_select": {"options": []}}, # Options can be added later if known
"Outbound Metriken": {"rich_text": {}},
}
},
"The Brain": {
"title": [{"type": "text", "text": {"content": "The Brain"}}],
"properties": {
"Titel": {"title": {}},
"Lösungsfragmente": {"rich_text": {}},
"Quelle": {"url": {}},
}
},
"GTM Workspace": {
"title": [{"type": "text", "text": {"content": "GTM Workspace"}}],
"properties": {
"Kampagnen Name": {"title": {}},
}
}
}
# --- Database Relations (Phase B) ---
# Define which databases relate to each other.
# The keys are the database names, and the values are lists of (property_name, related_database_name) tuples.
DATABASE_RELATIONS = {
"Product Master": [
("Sector Master", "Sector & Persona Master"),
("Messaging Matrix", "Messaging Matrix"),
("The Brain", "The Brain"),
("GTM Workspace", "GTM Workspace"),
],
"Sector & Persona Master": [
("Product Master", "Product Master"),
("Messaging Matrix", "Messaging Matrix"),
],
"Messaging Matrix": [
("Product Master", "Product Master"),
("Sector Master", "Sector & Persona Master"),
],
"The Brain": [
("Product Master", "Product Master"),
],
"GTM Workspace": [
("Product Master", "Product Master"),
],
# Competitive Radar and Enrichment Factory & RevOps do not have explicit relations to other *created* databases based on the document's "Notion Datenbank-Relationen" section.
}
# --- Helper Functions ---
def create_notion_database(parent_page_id, db_name, properties):
print(f"Attempting to create database: {db_name}")
create_url = f"{NOTION_API_BASE_URL}/databases"
payload = {
"parent": {"type": "page_id", "page_id": parent_page_id},
"title": DATABASE_SCHEMAS[db_name]["title"],
"properties": properties,
}
try:
response = requests.post(create_url, headers=HEADERS, json=payload)
response.raise_for_status() # Raise an exception for HTTP errors
db_data = response.json()
db_id = db_data["id"]
print(f"Successfully created database '{db_name}' with ID: {db_id}")
return db_id
except requests.exceptions.HTTPError as e:
print(f"HTTP Error creating database {db_name}: {e}")
if response is not None:
print(f"Response content: {response.text}")
return None
except Exception as e:
print(f"An unexpected error occurred while creating database {db_name}: {e}")
return None
def update_notion_database_relations(database_id, relations_to_add, created_db_ids):
print(f"Attempting to update relations for database ID: {database_id}")
update_url = f"{NOTION_API_BASE_URL}/databases/{database_id}"
properties_to_add = {}
for prop_name, related_db_name in relations_to_add:
if related_db_name in created_db_ids:
related_db_id = created_db_ids[related_db_name]
properties_to_add[prop_name] = {
"relation": {
"database_id": related_db_id,
"dual_property": {} # Notion automatically creates a dual property
}
}
else:
print(f"Warning: Related database '{related_db_name}' not found among created databases. Skipping relation for '{prop_name}'.")
if not properties_to_add:
print(f"No relations to add for database ID: {database_id}")
return False
payload = {
"properties": properties_to_add
}
try:
response = requests.patch(update_url, headers=HEADERS, json=payload)
response.raise_for_status()
print(f"Successfully updated relations for database ID: {database_id}")
return True
except requests.exceptions.HTTPError as e:
print(f"HTTP Error updating relations for database ID {database_id}: {e}")
if response is not None:
print(f"Response content: {response.text}")
return False
except Exception as e:
print(f"An unexpected error occurred while updating relations for database ID {database_id}: {e}")
return False
def main():
if NOTION_TOKEN == "YOUR_NOTION_TOKEN" or PARENT_PAGE_ID == "YOUR_PARENT_PAGE_ID":
print("ERROR: Please update NOTION_TOKEN and PARENT_PAGE_ID in the script before running.")
return
created_db_ids = {}
print("--- Phase A: Creating Databases ---")
for db_name, schema in DATABASE_SCHEMAS.items():
db_id = create_notion_database(PARENT_PAGE_ID, db_name, schema["properties"])
if db_id:
created_db_ids[db_name] = db_id
print(f"Waiting 15 seconds for Notion to index database '{db_name}'...")
time.sleep(15)
else:
print(f"Failed to create database: {db_name}. Aborting Phase A.")
return
print("\n--- Phase B: Establishing Relations ---")
if not created_db_ids:
print("No databases were created in Phase A. Cannot establish relations.")
return
for db_name, relations_config in DATABASE_RELATIONS.items():
if db_name in created_db_ids:
db_id = created_db_ids[db_name]
print(f"Processing relations for '{db_name}' (ID: {db_id})...")
if update_notion_database_relations(db_id, relations_config, created_db_ids):
print(f"Waiting 15 seconds after updating relations for '{db_name}'...")
time.sleep(15)
else:
print(f"Failed to update relations for: {db_name}. Continuing with other databases.")
else:
print(f"Warning: Database '{db_name}' not found in created IDs. Skipping relation updates.")
print("\n--- Setup Complete ---")
print("Please ensure your Notion integration has access to the parent page and its sub-pages in Notion UI.")
print("Created database IDs:")
for name, id_val in created_db_ids.items():
print(f"- {name}: {id_val}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,41 @@
import os
import requests
import json
# Get the Notion API key from the environment variable
api_key = os.environ.get("NOTION_API_KEY")
# If the API key is not set, try to read it from the file
if not api_key:
try:
with open("notion_token.txt", "r") as f:
api_key = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
print("Please set the NOTION_API_KEY environment variable or create the notion_token.txt file.")
exit()
# The ID of the page to retrieve
page_id = "2e288f42-8544-81d8-96f5-c231f84f719a" # Product Master
# The Notion API endpoint for retrieving a page
url = f"https://api.notion.com/v1/pages/{page_id}"
# The headers for the API request
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28",
}
# Make the API request
response = requests.get(url, headers=headers)
# Check the response status code
if response.status_code == 200:
# Print the response content
print(json.dumps(response.json(), indent=2))
else:
print(f"Error: {response.status_code}")
print(response.text)

View File

@@ -0,0 +1,161 @@
import requests
import json
import os
# --- Configuration ---
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
exit(1)
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# DB: Personas / Roles
DB_ID = "30588f42854480c38919e22d74d945ea"
# --- Data for Archetypes ---
archetypes = [
{
"name": "Wirtschaftlicher Entscheider",
"pains": [
"Steigende Personalkosten im Reinigungs- und Servicebereich gefährden Profitabilität.",
"Fachkräftemangel und Schwierigkeiten bei der Stellenbesetzung.",
"Inkonsistente Qualitätsstandards schaden dem Ruf des Hauses.",
"Hoher Managementaufwand für manuelle operative Prozesse."
],
"gains": [
"Reduktion operativer Personalkosten um 10-25%.",
"Deutliche Abnahme der Überstunden (bis zu 50%).",
"Sicherstellung konstant hoher Qualitätsstandards.",
"Erhöhung der operativen Effizienz durch präzise Datenanalysen."
],
"kpis": "Betriebskosten pro Einheit, Gästezufriedenheit (NPS), Mitarbeiterfluktuation.",
"positions": "Direktor, Geschäftsführer, C-Level, Einkaufsleiter."
},
{
"name": "Operativer Entscheider",
"pains": [
"Team ist überlastet und gestresst (Gefahr hoher Fluktuation).",
"Zu viele manuelle Routineaufgaben wie Abräumen oder Materialtransport.",
"Mangelnde Personalverfügbarkeit in Stoßzeiten führt zu Engpässen."
],
"gains": [
"Signifikante Entlastung des Personals von Routineaufgaben (20-40% Zeitgewinn).",
"Garantierte Reinigungszyklen unabhängig von Personalausfällen.",
"Mehr Zeit für wertschöpfende Aufgaben (Gästebetreuung, Upselling)."
],
"kpis": "Zeitaufwand für Routineaufgaben, Abdeckungsrate der Zyklen, Servicegeschwindigkeit.",
"positions": "Leiter Housekeeping, F&B Manager, Restaurantleiter, Stationsleitung."
},
{
"name": "Infrastruktur-Verantwortlicher",
"pains": [
"Technische Komplexität der Integration in bestehende Infrastruktur (Aufzüge, WLAN).",
"Sorge vor hohen Ausfallzeiten und unplanmäßigen Wartungskosten.",
"Fehlendes internes Fachpersonal für die Wartung autonomer Systeme."
],
"gains": [
"Reibungslose Integration (20-30% schnellere Implementierung).",
"Minimierung von Ausfallzeiten um 80-90% durch proaktives Monitoring.",
"Planbare Wartung und transparente Kosten durch feste SLAs."
],
"kpis": "System-Uptime, Implementierungszeit, Wartungskosten (TCO).",
"positions": "Technischer Leiter, Facility Manager, IT-Leiter."
},
{
"name": "Innovations-Treiber",
"pains": [
"Verlust der Wettbewerbsfähigkeit durch veraltete Prozesse.",
"Schwierigkeit das Unternehmen als modernen Arbeitgeber zu positionieren.",
"Statische Informations- und Marketingflächen werden oft ignoriert."
],
"gains": [
"Positionierung als Innovationsführer am Markt.",
"Steigerung der Kundeninteraktion um 20-30%.",
"Gewinnung wertvoller Daten zur kontinuierlichen Prozessoptimierung.",
"Erhöhte Attraktivität für junge, technikaffine Talente."
],
"kpis": "Besucherinteraktionsrate, Anzahl Prozessinnovationen, Modernitäts-Sentiment.",
"positions": "Marketingleiter, Center Manager, CDO, Business Development."
}
]
# --- Helper Functions ---
def format_rich_text(text):
return {"rich_text": [{"type": "text", "text": {"content": text}}]}
def format_title(text):
return {"title": [{"type": "text", "text": {"content": text}}]}
def find_page(title):
url = f"{NOTION_API_BASE_URL}/databases/{DB_ID}/query"
payload = {
"filter": {
"property": "Role",
"title": {"equals": title}
}
}
resp = requests.post(url, headers=HEADERS, json=payload)
resp.raise_for_status()
results = resp.json().get("results")
return results[0] if results else None
def create_page(properties):
url = f"{NOTION_API_BASE_URL}/pages"
payload = {
"parent": {"database_id": DB_ID},
"properties": properties
}
resp = requests.post(url, headers=HEADERS, json=payload)
resp.raise_for_status()
print("Created.")
def update_page(page_id, properties):
url = f"{NOTION_API_BASE_URL}/pages/{page_id}"
payload = {"properties": properties}
resp = requests.patch(url, headers=HEADERS, json=payload)
resp.raise_for_status()
print("Updated.")
# --- Main Logic ---
def main():
print(f"Syncing {len(archetypes)} Personas to Notion DB {DB_ID}...")
for p in archetypes:
print(f"Processing '{p['name']}'...")
pains_text = "\n".join([f"- {item}" for item in p["pains"]])
gains_text = "\n".join([f"- {item}" for item in p["gains"]])
properties = {
"Role": format_title(p["name"]),
"Pains": format_rich_text(pains_text),
"Gains": format_rich_text(gains_text),
"KPIs": format_rich_text(p.get("kpis", "")),
"Typische Positionen": format_rich_text(p.get("positions", ""))
}
existing_page = find_page(p["name"])
if existing_page:
print(f" -> Found existing page {existing_page['id']}. Updating...")
update_page(existing_page["id"], properties)
else:
print(" -> Creating new page...")
create_page(properties)
print("Sync complete.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,145 @@
import requests
import json
import os
import re
import sys
TOKEN_FILE = 'notion_api_key.txt'
PARENT_PAGE_ID = "2e088f42-8544-8024-8289-deb383da3818"
def parse_markdown_to_blocks(md_content):
blocks = []
lines = md_content.split('\n')
in_code_block = False
code_content = []
for line in lines:
stripped = line.strip()
if stripped.startswith("```"):
if in_code_block:
blocks.append({
"object": "block",
"type": "code",
"code": {
"rich_text": [{"type": "text", "text": {"content": '\n'.join(code_content)}}],
"language": "plain text"
}
})
code_content = []
in_code_block = False
else:
in_code_block = True
continue
if in_code_block:
code_content.append(line)
continue
if not stripped:
continue
if line.startswith("# "):
blocks.append({
"object": "block",
"type": "heading_1",
"heading_1": {"rich_text": [{"type": "text", "text": {"content": line[2:]}}]}}
)
elif line.startswith("## "):
blocks.append({
"object": "block",
"type": "heading_2",
"heading_2": {"rich_text": [{"type": "text", "text": {"content": line[3:]}}]}}
)
elif line.startswith("### "):
blocks.append({
"object": "block",
"type": "heading_3",
"heading_3": {"rich_text": [{"type": "text", "text": {"content": line[4:]}}]}}
)
elif stripped.startswith("* ") or stripped.startswith("- "):
content = stripped[2:]
blocks.append({
"object": "block",
"type": "bulleted_list_item",
"bulleted_list_item": {"rich_text": [{"type": "text", "text": {"content": content}}]}}
)
elif re.match(r"^\d+\.", stripped):
content = re.sub(r"^\d+\.\s*", "", stripped)
blocks.append({
"object": "block",
"type": "numbered_list_item",
"numbered_list_item": {"rich_text": [{"type": "text", "text": {"content": content}}]}}
)
elif stripped.startswith("|"):
blocks.append({
"object": "block",
"type": "code",
"code": {
"rich_text": [{"type": "text", "text": {"content": line}}],
"language": "plain text"
}
})
else:
blocks.append({
"object": "block",
"type": "paragraph",
"paragraph": {"rich_text": [{"type": "text", "text": {"content": line}}]}}
)
return blocks
def upload_doc(token, file_path):
try:
with open(file_path, 'r') as f:
content = f.read()
except FileNotFoundError:
print(f"Error: Could not find '{file_path}'")
return
title = os.path.basename(file_path)
if content.startswith("# "):
title = content.split('\n')[0][2:].strip()
print(f"Parsing '{file_path}'...")
children_blocks = parse_markdown_to_blocks(content)
url = "https://api.notion.com/v1/pages"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
payload = {
"parent": { "page_id": PARENT_PAGE_ID },
"properties": {
"title": [{"text": {"content": f"📘 {title}"}}]
},
"children": children_blocks[:100]
}
print(f"Uploading '{title}' to Notion...")
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
print(f"SUCCESS: {data.get('url')}")
except Exception as e:
print(f"ERROR: {e}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python sync_docs_to_notion.py <filename>")
sys.exit(1)
try:
with open(TOKEN_FILE, 'r') as f:
token = f.read().strip()
except FileNotFoundError:
print(f"Error: Could not find '{TOKEN_FILE}'")
sys.exit(1)
upload_doc(token, sys.argv[1])

View File

@@ -0,0 +1,150 @@
import requests
import json
# --- Configuration ---
try:
with open("notion_token.txt", "r") as f:
NOTION_TOKEN = f.read().strip()
except FileNotFoundError:
print("Error: notion_token.txt not found.")
exit(1)
NOTION_VERSION = "2022-06-28"
NOTION_API_BASE_URL = "https://api.notion.com/v1"
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
# DB: Sector & Persona Master
DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b"
# --- Data ---
archetypes = [
{
"name": "Wirtschaftlicher Entscheider",
"pains": [
"Steigende operative Personalkosten und Fachkräftemangel gefährden die Profitabilität.",
"Unklare Amortisation (ROI) und Risiko von Fehlinvestitionen bei neuen Technologien.",
"Intransparente Folgekosten (TCO) und schwierige Budgetplanung über die Lebensdauer."
],
"gains": [
"Nachweisbare Senkung der operativen Kosten (10-25%) und schnelle Amortisation.",
"Sicherung der Wettbewerbsfähigkeit durch effizientere Kostenstrukturen.",
"Volle Transparenz und Planbarkeit durch klare Service-Modelle (SLAs)."
]
},
{
"name": "Operativer Entscheider",
"pains": [
"Personelle Unterbesetzung führt zu Überstunden, Stress und Qualitätsmängeln.",
"Wiederkehrende Routineaufgaben binden wertvolle Fachkräfte-Ressourcen.",
"Schwierigkeit, gleichbleibend hohe Standards (Hygiene/Service) 24/7 zu garantieren."
],
"gains": [
"Spürbare Entlastung des Teams von Routineaufgaben (20-40%).",
"Garantierte, gleichbleibend hohe Ausführungsqualität unabhängig von der Tagesform.",
"Stabilisierung der operativen Abläufe und Kompensation von Personalausfällen."
]
},
{
"name": "Infrastruktur-Verantwortlicher",
"pains": [
"Sorge vor komplexer Integration in bestehende IT- und Gebäudeinfrastruktur (WLAN, Türen, Aufzüge).",
"Risiko von hohen Ausfallzeiten und aufwändiger Fehlerbehebung ohne internes Spezialwissen.",
"Unklare Wartungsaufwände und Angst vor 'Insel-Lösungen' ohne Schnittstellen."
],
"gains": [
"Reibungslose, fachgerechte Integration durch Experten-Support (Plug & Play).",
"Maximale Betriebssicherheit durch proaktives Monitoring und schnelle Reaktionszeiten.",
"Zentrales Management und volle Transparenz über Systemstatus und Wartungsbedarf."
]
},
{
"name": "Innovations-Treiber",
"pains": [
"Verlust der Attraktivität als moderner Arbeitgeber oder Dienstleister (Veraltetes Image).",
"Fehlende 'Wow-Effekte' in der Kundeninteraktion und mangelnde Differenzierung vom Wettbewerb.",
"Verpasste Chancen durch fehlende Datengrundlage für digitale Optimierungen."
],
"gains": [
"Positionierung als Innovationsführer und Steigerung der Markenattraktivität.",
"Schaffung einzigartiger Kundenerlebnisse durch sichtbare High-Tech-Lösungen.",
"Gewinnung wertvoller Daten zur kontinuierlichen Prozessoptimierung und Digitalisierung."
]
}
]
# --- Helper Functions ---
def format_rich_text(text):
return {"rich_text": [{"type": "text", "text": {"content": text}}]}
def format_title(text):
return {"title": [{"type": "text", "text": {"content": text}}]}
def find_page(title):
url = f"{NOTION_API_BASE_URL}/databases/{DB_ID}/query"
payload = {
"filter": {
"property": "Name",
"title": {"equals": title}
}
}
resp = requests.post(url, headers=HEADERS, json=payload)
resp.raise_for_status()
results = resp.json().get("results")
return results[0] if results else None
def create_page(properties):
url = f"{NOTION_API_BASE_URL}/pages"
payload = {
"parent": {"database_id": DB_ID},
"properties": properties
}
resp = requests.post(url, headers=HEADERS, json=payload)
resp.raise_for_status()
print("Created.")
def update_page(page_id, properties):
url = f"{NOTION_API_BASE_URL}/pages/{page_id}"
payload = {"properties": properties}
resp = requests.patch(url, headers=HEADERS, json=payload)
resp.raise_for_status()
print("Updated.")
# --- Main Sync Loop ---
def main():
print(f"Syncing {len(archetypes)} Personas to Notion DB {DB_ID}...")
for p in archetypes:
print(f"Processing '{p['name']}'...")
# Format Pains/Gains as lists with bullets for Notion Text field
pains_text = "\n".join([f"- {item}" for item in p["pains"]])
gains_text = "\n".join([f"- {item}" for item in p["gains"]])
properties = {
"Name": format_title(p["name"]),
"Pains": format_rich_text(pains_text),
"Gains": format_rich_text(gains_text),
# Optional: Add a tag to distinguish them from Sectors if needed?
# Currently just relying on Name uniqueness.
}
existing_page = find_page(p["name"])
if existing_page:
print(f" -> Found existing page {existing_page['id']}. Updating...")
update_page(existing_page["id"], properties)
else:
print(" -> Creating new page...")
create_page(properties)
print("Sync complete.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,92 @@
import requests
import json
import os
import sys
# Use the same token file as the other scripts
TOKEN_FILE = 'notion_token.txt'
def get_notion_token():
"""Reads the Notion API token from the specified file."""
try:
with open(TOKEN_FILE, 'r') as f:
return f.read().strip()
except FileNotFoundError:
print(f"Error: Token file not found at '{TOKEN_FILE}'")
print("Please create this file and place your Notion Integration Token inside.")
sys.exit(1)
def parse_markdown_to_blocks(md_content):
"""
Parses a simple markdown string into Notion API block objects.
This is a simplified parser for this specific task.
"""
blocks = []
lines = md_content.split('\n')
for line in lines:
stripped = line.strip()
if line.startswith("# "):
blocks.append({ "object": "block", "type": "heading_1", "heading_1": {"rich_text": [{"type": "text", "text": {"content": line[2:]}}]}})
elif line.startswith("## "):
blocks.append({ "object": "block", "type": "heading_2", "heading_2": {"rich_text": [{"type": "text", "text": {"content": line[3:]}}]}})
elif stripped.startswith("* ") or stripped.startswith("- "):
blocks.append({ "object": "block", "type": "bulleted_list_item", "bulleted_list_item": {"rich_text": [{"type": "text", "text": {"content": stripped[2:]}}]}})
elif stripped: # Any non-empty line becomes a paragraph
blocks.append({ "object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": line}}]}})
# Add a divider for visual separation
blocks.insert(0, {"type": "divider", "divider": {}})
blocks.insert(0, {
"object": "block", "type": "heading_2", "heading_2": {
"rich_text": [{"type": "text", "text": {"content": "Gemini Task-Update:"}}]
}
})
return blocks
def append_blocks_to_page(token, page_id, blocks):
"""
Appends a list of block objects to a Notion page.
"""
# In Notion, the page ID is the block ID for appending content
url = f"https://api.notion.com/v1/blocks/{page_id}/children"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
payload = {"children": blocks}
print(f"Appending {len(blocks)} blocks to Notion Page ID: {page_id}...")
try:
response = requests.patch(url, headers=headers, json=payload)
response.raise_for_status()
print("SUCCESS: Content appended to Notion task.")
except requests.exceptions.HTTPError as e:
print(f"ERROR: Failed to update Notion page. Response: {e.response.text}")
sys.exit(1)
except Exception as e:
print(f"ERROR: An unexpected error occurred: {e}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python update_notion_task.py <page_id> \"<content_string>\"")
print("Example: python update_notion_task.py 12345-abc... \"- Task 1\n- Task 2\"")
sys.exit(1)
page_id = sys.argv[1]
content_to_append = sys.argv[2]
# Basic validation for page_id
if not isinstance(page_id, str) or len(page_id) < 32:
print(f"Error: Invalid Page ID provided: '{page_id}'")
sys.exit(1)
notion_token = get_notion_token()
content_blocks = parse_markdown_to_blocks(content_to_append)
append_blocks_to_page(notion_token, page_id, content_blocks)

View File

@@ -0,0 +1,131 @@
import sys
import os
import json
# Setup Environment to import backend modules
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
from backend.database import SessionLocal, Persona, JobRolePattern
def seed_archetypes():
db = SessionLocal()
print("Seeding Strategic Archetypes (Pains & Gains)...")
# --- 1. The 4 Strategic Archetypes ---
# Based on user input and synthesis of previous specific roles
archetypes = [
{
"name": "Operativer Entscheider",
"pains": [
"Personelle Unterbesetzung und hohe Fluktuation führen zu Überstunden und Qualitätsmängeln.",
"Manuelle, wiederkehrende Prozesse binden wertvolle Ressourcen und senken die Effizienz.",
"Sicherstellung gleichbleibend hoher Standards (Hygiene/Service) ist bei Personalmangel kaum möglich."
],
"gains": [
"Spürbare Entlastung des Teams von Routineaufgaben (20-40%).",
"Garantierte, gleichbleibend hohe Ausführungsqualität rund um die Uhr.",
"Stabilisierung der operativen Abläufe unabhängig von kurzfristigen Personalausfällen."
]
},
{
"name": "Infrastruktur-Verantwortlicher",
"pains": [
"Integration neuer Systeme in bestehende Gebäude/IT ist oft komplex und risikobehaftet.",
"Sorge vor hohen Ausfallzeiten und aufwändiger Fehlerbehebung ohne internes Spezialwissen.",
"Unklare Wartungsaufwände und Schnittstellenprobleme (WLAN, Aufzüge, Türen)."
],
"gains": [
"Reibungslose, fachgerechte Integration in die bestehende Infrastruktur.",
"Maximale Betriebssicherheit durch proaktives Monitoring und schnelle Reaktionszeiten.",
"Volle Transparenz über Systemstatus und Wartungsbedarf."
]
},
{
"name": "Wirtschaftlicher Entscheider",
"pains": [
"Steigende operative Kosten (Personal, Material) drücken auf die Margen.",
"Unklare Amortisation (ROI) und Risiko von Fehlinvestitionen bei neuen Technologien.",
"Intransparente Folgekosten (TCO) über die Lebensdauer der Anlagen."
],
"gains": [
"Nachweisbare Senkung der operativen Kosten (10-25%).",
"Transparente und planbare Kostenstruktur (TCO) ohne versteckte Überraschungen.",
"Schneller, messbarer Return on Investment durch Effizienzsteigerung."
]
},
{
"name": "Innovations-Treiber",
"pains": [
"Verlust der Wettbewerbsfähigkeit durch veraltete Prozesse und Kundenangebote.",
"Schwierigkeit, das Unternehmen als modernes, zukunftsorientiertes Brand zu positionieren.",
"Verpasste Chancen durch fehlende Datengrundlage für Optimierungen."
],
"gains": [
"Positionierung als Innovationsführer und Steigerung der Arbeitgeberattraktivität.",
"Nutzung modernster Technologie als sichtbares Differenzierungsmerkmal.",
"Gewinnung wertvoller Daten zur kontinuierlichen Prozessoptimierung."
]
}
]
# Clear existing Personas to avoid mix-up with old granular ones
# (In production, we might want to be more careful, but here we want a clean slate for the new archetypes)
try:
db.query(Persona).delete()
db.commit()
print("Cleared old Personas.")
except Exception as e:
print(f"Warning clearing personas: {e}")
for p_data in archetypes:
print(f"Creating Archetype: {p_data['name']}")
new_persona = Persona(
name=p_data["name"],
pains=json.dumps(p_data["pains"]),
gains=json.dumps(p_data["gains"])
)
db.add(new_persona)
db.commit()
# --- 2. Update JobRolePatterns to map to Archetypes ---
# We map the patterns to the new 4 Archetypes
mapping_updates = [
# Wirtschaftlicher Entscheider
{"role": "Wirtschaftlicher Entscheider", "patterns": ["geschäftsführer", "ceo", "director", "einkauf", "procurement", "finance", "cfo"]},
# Operativer Entscheider
{"role": "Operativer Entscheider", "patterns": ["housekeeping", "hausdame", "hauswirtschaft", "reinigung", "restaurant", "f&b", "werksleiter", "produktionsleiter", "lager", "logistik", "operations", "coo"]},
# Infrastruktur-Verantwortlicher
{"role": "Infrastruktur-Verantwortlicher", "patterns": ["facility", "technik", "instandhaltung", "it-leiter", "cto", "admin", "building"]},
# Innovations-Treiber
{"role": "Innovations-Treiber", "patterns": ["innovation", "digital", "transformation", "business dev", "marketing"]}
]
# Clear old mappings to prevent confusion
db.query(JobRolePattern).delete()
db.commit()
print("Cleared old JobRolePatterns.")
for group in mapping_updates:
role_name = group["role"]
for pattern_text in group["patterns"]:
print(f"Mapping '{pattern_text}' -> '{role_name}'")
# All seeded patterns are regex contains checks
new_pattern = JobRolePattern(
pattern_type='regex',
pattern_value=pattern_text, # Stored without wildcards
role=role_name,
priority=100, # Default priority for seeded patterns
created_by='system'
)
db.add(new_pattern)
db.commit()
print("Archetypes and Mappings Seeded Successfully.")
db.close()
if __name__ == "__main__":
seed_archetypes()

View File

@@ -67,6 +67,7 @@ def extract_select(prop):
return prop.get("select", {}).get("name", "") if prop.get("select") else ""
def extract_number(prop):
if not prop: return None
return prop.get("number")
def sync_categories(token, session):
@@ -135,6 +136,11 @@ def sync_industries(token, session):
industry.name = name
industry.description = extract_rich_text(props.get("Definition"))
# New: Map Pains & Gains explicitly
industry.pains = extract_rich_text(props.get("Pains"))
industry.gains = extract_rich_text(props.get("Gains"))
industry.notes = extract_rich_text(props.get("Notes"))
status = extract_select(props.get("Status"))
industry.status_notion = status
industry.is_focus = (status == "P1 Focus Industry")
@@ -147,6 +153,12 @@ def sync_industries(token, session):
industry.scraper_search_term = extract_select(props.get("Scraper Search Term")) # <-- FIXED HERE
industry.scraper_keywords = extract_rich_text(props.get("Scraper Keywords"))
industry.standardization_logic = extract_rich_text(props.get("Standardization Logic"))
# New Field: Ops Focus Secondary (Checkbox)
industry.ops_focus_secondary = props.get("Ops Focus: Secondary", {}).get("checkbox", False)
# New Field: Strategy Briefing (Miller Heiman)
industry.strategy_briefing = extract_rich_text(props.get("Strategy Briefing"))
# Relation: Primary Product Category
relation = props.get("Primary Product Category", {}).get("relation", [])
@@ -157,6 +169,16 @@ def sync_industries(token, session):
industry.primary_category_id = cat.id
else:
logger.warning(f"Related category {related_id} not found for industry {name}")
# Relation: Secondary Product Category
relation_sec = props.get("Secondary Product", {}).get("relation", [])
if relation_sec:
related_id = relation_sec[0]["id"]
cat = session.query(RoboticsCategory).filter(RoboticsCategory.notion_id == related_id).first()
if cat:
industry.secondary_category_id = cat.id
else:
logger.warning(f"Related Secondary category {related_id} not found for industry {name}")
count += 1

View File

@@ -0,0 +1,149 @@
import sys
import os
import requests
import json
import logging
# Add company-explorer to path (parent of backend)
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from backend.database import SessionLocal, Persona, init_db
from backend.config import settings
# Setup Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
NOTION_TOKEN_FILE = "/app/notion_token.txt"
# Sector & Persona Master DB
PERSONAS_DB_ID = "30588f42-8544-80c3-8919-e22d74d945ea"
VALID_ARCHETYPES = {
"Wirtschaftlicher Entscheider",
"Operativer Entscheider",
"Infrastruktur-Verantwortlicher",
"Innovations-Treiber",
"Influencer"
}
def load_notion_token():
try:
with open(NOTION_TOKEN_FILE, "r") as f:
return f.read().strip()
except FileNotFoundError:
logger.error(f"Notion token file not found at {NOTION_TOKEN_FILE}")
sys.exit(1)
def query_notion_db(token, db_id):
url = f"https://api.notion.com/v1/databases/{db_id}/query"
headers = {
"Authorization": f"Bearer {token}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
}
results = []
has_more = True
next_cursor = None
while has_more:
payload = {}
if next_cursor:
payload["start_cursor"] = next_cursor
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200:
logger.error(f"Error querying Notion DB {db_id}: {response.text}")
break
data = response.json()
results.extend(data.get("results", []))
has_more = data.get("has_more", False)
next_cursor = data.get("next_cursor")
return results
def extract_title(prop):
if not prop: return ""
return "".join([t.get("plain_text", "") for t in prop.get("title", [])])
def extract_rich_text(prop):
if not prop: return ""
return "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])])
def extract_rich_text_to_list(prop):
"""
Extracts rich text and converts bullet points/newlines into a list of strings.
"""
if not prop: return []
full_text = "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])])
# Split by newline and clean up bullets
lines = full_text.split('\n')
cleaned_lines = []
for line in lines:
line = line.strip()
if not line: continue
if line.startswith("- "):
line = line[2:]
elif line.startswith(""):
line = line[2:]
cleaned_lines.append(line)
return cleaned_lines
def sync_personas(token, session):
logger.info("Syncing Personas from Notion...")
pages = query_notion_db(token, PERSONAS_DB_ID)
count = 0
for page in pages:
props = page.get("properties", {})
# The title property is 'Role' in the new DB, not 'Name'
name = extract_title(props.get("Role"))
if name not in VALID_ARCHETYPES:
logger.debug(f"Skipping '{name}' (Not a target Archetype)")
continue
logger.info(f"Processing Persona: {name}")
pains_list = extract_rich_text_to_list(props.get("Pains"))
gains_list = extract_rich_text_to_list(props.get("Gains"))
description = extract_rich_text(props.get("Rollenbeschreibung"))
convincing_arguments = extract_rich_text(props.get("Was ihn überzeugt"))
typical_positions = extract_rich_text(props.get("Typische Positionen"))
kpis = extract_rich_text(props.get("KPIs"))
# Upsert Logic
persona = session.query(Persona).filter(Persona.name == name).first()
if not persona:
persona = Persona(name=name)
session.add(persona)
logger.info(f" -> Creating new entry")
else:
logger.info(f" -> Updating existing entry")
persona.pains = json.dumps(pains_list, ensure_ascii=False)
persona.gains = json.dumps(gains_list, ensure_ascii=False)
persona.description = description
persona.convincing_arguments = convincing_arguments
persona.typical_positions = typical_positions
persona.kpis = kpis
count += 1
session.commit()
logger.info(f"Sync complete. Updated {count} personas.")
if __name__ == "__main__":
token = load_notion_token()
db = SessionLocal()
try:
sync_personas(token, db)
except Exception as e:
logger.error(f"Sync failed: {e}", exc_info=True)
finally:
db.close()

View File

@@ -7,7 +7,7 @@ import logging
# /app/backend/scripts/sync.py -> /app
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from backend.database import SessionLocal, Industry, RoboticsCategory, init_db
from backend.database import SessionLocal, Industry, RoboticsCategory, Persona, init_db
from dotenv import load_dotenv
# Try loading from .env in root if exists
@@ -76,6 +76,21 @@ def extract_number(prop):
if not prop or "number" not in prop: return None
return prop["number"]
def extract_rich_text_to_list(prop):
if not prop or "rich_text" not in prop: return []
full_text = "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])])
lines = full_text.split('\n')
cleaned_lines = []
for line in lines:
line = line.strip()
if not line: continue
if line.startswith("- "):
line = line[2:]
elif line.startswith(""):
line = line[2:]
cleaned_lines.append(line)
return cleaned_lines
def sync():
logger.info("--- Starting Enhanced Sync ---")
@@ -83,6 +98,48 @@ def sync():
init_db()
session = SessionLocal()
# --- 4. Sync Personas (NEW) ---
# Sector & Persona Master ID
PERSONAS_DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b"
VALID_ARCHETYPES = {
"Wirtschaftlicher Entscheider",
"Operativer Entscheider",
"Infrastruktur-Verantwortlicher",
"Innovations-Treiber"
}
if PERSONAS_DB_ID:
logger.info(f"Syncing Personas from {PERSONAS_DB_ID}...")
pages = query_all(PERSONAS_DB_ID)
p_count = 0
# We assume Personas are cumulative, so we don't delete all first (safer for IDs)
# But we could if we wanted a clean slate. Upsert is better.
for page in pages:
props = page["properties"]
name = extract_title(props.get("Name"))
if name not in VALID_ARCHETYPES:
continue
import json
pains_list = extract_rich_text_to_list(props.get("Pains"))
gains_list = extract_rich_text_to_list(props.get("Gains"))
persona = session.query(Persona).filter(Persona.name == name).first()
if not persona:
persona = Persona(name=name)
session.add(persona)
persona.pains = json.dumps(pains_list, ensure_ascii=False)
persona.gains = json.dumps(gains_list, ensure_ascii=False)
p_count += 1
session.commit()
logger.info(f"✅ Synced {p_count} Personas.")
# 2. Sync Categories (Products)
cat_db_id = find_db_id("Product Categories") or find_db_id("Products")
if cat_db_id:

View File

@@ -0,0 +1,47 @@
import sys
import os
# Setup Environment
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
from backend.database import SessionLocal, JobRolePattern, Persona
def test_mapping(job_title):
db = SessionLocal()
print(f"\n--- Testing Mapping for '{job_title}' ---")
# 1. Find Role Name via JobRolePattern
role_name = None
mappings = db.query(JobRolePattern).all()
for m in mappings:
pattern_clean = m.pattern.replace("%", "").lower()
if pattern_clean in job_title.lower():
role_name = m.role
print(f" -> Matched Pattern: '{m.pattern}' => Role: '{role_name}'")
break
if not role_name:
print(" -> No Pattern Matched.")
return
# 2. Find Persona via Role Name
persona = db.query(Persona).filter(Persona.name == role_name).first()
if persona:
print(f" -> Found Persona ID: {persona.id} (Name: {persona.name})")
else:
print(f" -> ERROR: Persona '{role_name}' not found in DB!")
db.close()
if __name__ == "__main__":
test_titles = [
"Leiter Hauswirtschaft",
"CTO",
"Geschäftsführer",
"Head of Marketing",
"Einkaufsleiter"
]
for t in test_titles:
test_mapping(t)

View File

@@ -0,0 +1,41 @@
import sys
import os
import logging
# Add backend path
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
# Mock logging
logging.basicConfig(level=logging.INFO)
# Import Service
from backend.services.classification import ClassificationService
def test_opener_generation():
service = ClassificationService()
print("\n--- TEST: Therme Erding (Primary Focus: Hygiene) ---")
op_prim = service._generate_marketing_opener(
company_name="Therme Erding",
website_text="Größte Therme der Welt, 35 Saunen, Rutschenparadies Galaxy, Wellenbad. Täglich tausende Besucher.",
industry_name="Leisure - Wet & Spa",
industry_pains="Rutschgefahr und Hygiene",
focus_mode="primary"
)
print(f"Primary Opener: {op_prim}")
print("\n--- TEST: Dachser Logistik (Secondary Focus: Process) ---")
op_sec = service._generate_marketing_opener(
company_name="Dachser SE",
website_text="Globaler Logistikdienstleister, Warehousing, Food Logistics, Air & Sea Logistics. Intelligent Logistics.",
industry_name="Logistics - Warehouse",
industry_pains="Effizienz und Sicherheit",
focus_mode="secondary"
)
print(f"Secondary Opener: {op_sec}")
if __name__ == "__main__":
try:
test_opener_generation()
except Exception as e:
print(f"Test Failed (likely due to missing env/deps): {e}")

View File

@@ -0,0 +1,67 @@
import requests
import os
import time
import argparse
import sys
import logging
# Add the backend directory to the Python path for relative imports to work
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# --- Configuration ---
def load_env_manual(path):
if not os.path.exists(path):
# print(f"⚠️ Warning: .env file not found at {path}") # Suppress for cleaner output in container
return
with open(path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, val = line.split('=', 1)
os.environ.setdefault(key.strip(), val.strip())
# Load .env (assuming it's in /app) - this needs to be run from /app or adjusted
# For docker-compose exec from project root, /app is the container's WORKDIR
load_env_manual('/app/.env')
API_USER = os.getenv("API_USER")
API_PASS = os.getenv("API_PASSWORD")
# When run INSIDE the container, the service is reachable via localhost
CE_URL = "http://localhost:8000"
ANALYZE_ENDPOINT = f"{CE_URL}/api/enrich/analyze"
def trigger_analysis(company_id: int):
print("="*60)
print(f"🚀 Triggering REAL analysis for Company ID: {company_id}")
print("="*60)
payload = {"company_id": company_id}
try:
# Added logging for API user/pass (debug only, remove in prod)
logger.debug(f"API Call to {ANALYZE_ENDPOINT} with user {API_USER}")
response = requests.post(ANALYZE_ENDPOINT, json=payload, auth=(API_USER, API_PASS), timeout=30) # Increased timeout
if response.status_code == 200 and response.json().get("status") == "queued":
print(" ✅ SUCCESS: Analysis task has been queued on the server.")
print(" The result will be available in the database and UI shortly.")
return True
else:
print(f" ❌ FAILURE: Server responded with status {response.status_code}")
print(f" Response: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f" ❌ FATAL: Could not connect to the server: {e}")
return False
if __name__ == "__main__":
# Add a basic logger to the script itself for clearer output
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
parser = argparse.ArgumentParser(description="Trigger Company Explorer Analysis Task")
parser.add_argument("--company-id", type=int, required=True, help="ID of the company to analyze")
args = parser.parse_args()
trigger_analysis(args.company_id)

View File

@@ -0,0 +1,33 @@
import sys
import os
# Add parent directory to path to allow import of backend.database
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
# Import everything to ensure metadata is populated
from backend.database import engine, Base, Company, Contact, Industry, JobRolePattern, Persona, Signal, EnrichmentData, RoboticsCategory, ImportLog, ReportedMistake, MarketingMatrix
def migrate():
print("Migrating Database Schema...")
try:
# Hacky migration for MarketingMatrix: Drop if exists to enforce new schema
with engine.connect() as con:
print("Dropping old MarketingMatrix table to enforce schema change...")
try:
from sqlalchemy import text
con.execute(text("DROP TABLE IF EXISTS marketing_matrix"))
print("Dropped marketing_matrix.")
except Exception as e:
print(f"Could not drop marketing_matrix: {e}")
except Exception as e:
print(f"Pre-migration cleanup error: {e}")
# This creates 'personas' table AND re-creates 'marketing_matrix'
Base.metadata.create_all(bind=engine)
print("Migration complete. 'personas' table created and 'marketing_matrix' refreshed.")
if __name__ == "__main__":
migrate()