[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:
0
company-explorer/backend/scripts/__init__.py
Normal file
0
company-explorer/backend/scripts/__init__.py
Normal file
44
company-explorer/backend/scripts/add_unsubscribe_tokens.py
Normal file
44
company-explorer/backend/scripts/add_unsubscribe_tokens.py
Normal 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.")
|
||||
@@ -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()
|
||||
22
company-explorer/backend/scripts/check_mappings.py
Normal file
22
company-explorer/backend/scripts/check_mappings.py
Normal 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()
|
||||
171
company-explorer/backend/scripts/classify_unmapped_titles.py
Normal file
171
company-explorer/backend/scripts/classify_unmapped_titles.py
Normal 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 ---")
|
||||
64
company-explorer/backend/scripts/debug_check_matrix_texts.py
Normal file
64
company-explorer/backend/scripts/debug_check_matrix_texts.py
Normal 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)
|
||||
94
company-explorer/backend/scripts/debug_single_company.py
Normal file
94
company-explorer/backend/scripts/debug_single_company.py
Normal 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.")
|
||||
307
company-explorer/backend/scripts/generate_matrix.py
Normal file
307
company-explorer/backend/scripts/generate_matrix.py
Normal 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)
|
||||
66
company-explorer/backend/scripts/import_job_titles.py
Normal file
66
company-explorer/backend/scripts/import_job_titles.py
Normal 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)
|
||||
58
company-explorer/backend/scripts/inspect_sqlite_native.py
Normal file
58
company-explorer/backend/scripts/inspect_sqlite_native.py
Normal 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")
|
||||
58
company-explorer/backend/scripts/inspect_therme.py
Normal file
58
company-explorer/backend/scripts/inspect_therme.py
Normal 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")
|
||||
@@ -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.")
|
||||
|
||||
|
||||
43
company-explorer/backend/scripts/migrate_matrix_campaign.py
Normal file
43
company-explorer/backend/scripts/migrate_matrix_campaign.py
Normal 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()
|
||||
31
company-explorer/backend/scripts/migrate_opener.py
Normal file
31
company-explorer/backend/scripts/migrate_opener.py
Normal 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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
263
company-explorer/backend/scripts/notion_tools/import_product.py
Normal file
263
company-explorer/backend/scripts/notion_tools/import_product.py
Normal 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()
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
220
company-explorer/backend/scripts/notion_tools/notion_db_setup.py
Normal file
220
company-explorer/backend/scripts/notion_tools/notion_db_setup.py
Normal 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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
131
company-explorer/backend/scripts/seed_marketing_data.py
Normal file
131
company-explorer/backend/scripts/seed_marketing_data.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
|
||||
149
company-explorer/backend/scripts/sync_notion_personas.py
Normal file
149
company-explorer/backend/scripts/sync_notion_personas.py
Normal 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()
|
||||
@@ -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:
|
||||
|
||||
47
company-explorer/backend/scripts/test_mapping_logic.py
Normal file
47
company-explorer/backend/scripts/test_mapping_logic.py
Normal 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)
|
||||
41
company-explorer/backend/scripts/test_opener_generation.py
Normal file
41
company-explorer/backend/scripts/test_opener_generation.py
Normal 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}")
|
||||
67
company-explorer/backend/scripts/trigger_analysis.py
Normal file
67
company-explorer/backend/scripts/trigger_analysis.py
Normal 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)
|
||||
33
company-explorer/backend/scripts/upgrade_schema_v2.py
Normal file
33
company-explorer/backend/scripts/upgrade_schema_v2.py
Normal 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()
|
||||
Reference in New Issue
Block a user