fix(content): implement state refresh on update to prevent data loss on tab switch
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
@@ -15,14 +14,34 @@ sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
from helpers import call_gemini_flash, scrape_website_details
|
||||
from config import Config
|
||||
|
||||
LOG_DIR = "Log_from_docker"
|
||||
# --- LOGGING CONFIGURATION ---
|
||||
LOG_DIR = "/app/Log_from_docker"
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.makedirs(LOG_DIR)
|
||||
try:
|
||||
os.makedirs(LOG_DIR)
|
||||
except:
|
||||
pass # Should be mounted
|
||||
|
||||
run_timestamp = datetime.now().strftime("%y-%m-%d_%H-%M-%S")
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
run_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
log_file = os.path.join(LOG_DIR, f"content_python_debug.log")
|
||||
|
||||
Config.load_api_keys()
|
||||
# Configure root logger
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler(sys.stderr) # Write logs to stderr so stdout keeps clean JSON
|
||||
]
|
||||
)
|
||||
|
||||
logging.info(f"--- Content Orchestrator Started ({run_timestamp}) ---")
|
||||
|
||||
try:
|
||||
Config.load_api_keys()
|
||||
logging.info("API Keys loaded successfully.")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load API keys: {e}")
|
||||
|
||||
def get_copywriter_instruction(lang='de'):
|
||||
return r"""
|
||||
@@ -39,112 +58,161 @@ def get_copywriter_instruction(lang='de'):
|
||||
# --- MODES ---
|
||||
|
||||
def list_gtm_projects(payload):
|
||||
projects = db_manager.get_all_gtm_projects()
|
||||
return {"projects": projects}
|
||||
logging.info("Executing list_gtm_projects")
|
||||
try:
|
||||
projects = db_manager.get_all_gtm_projects()
|
||||
logging.debug(f"Found {len(projects)} GTM projects")
|
||||
return {"projects": projects}
|
||||
except Exception as e:
|
||||
logging.error(f"Error listing GTM projects: {e}", exc_info=True)
|
||||
return {"error": str(e), "projects": []}
|
||||
|
||||
def import_project(payload):
|
||||
logging.info(f"Executing import_project with payload keys: {list(payload.keys())}")
|
||||
gtm_id = payload.get('gtmProjectId')
|
||||
|
||||
if not gtm_id:
|
||||
logging.error("Missing gtmProjectId in payload")
|
||||
return {"error": "Missing gtmProjectId"}
|
||||
|
||||
result = db_manager.import_gtm_project(gtm_id)
|
||||
if not result:
|
||||
return {"error": "GTM Project not found or import failed"}
|
||||
|
||||
return result
|
||||
try:
|
||||
logging.info(f"Attempting to import GTM Project ID: {gtm_id}")
|
||||
result = db_manager.import_gtm_project(gtm_id)
|
||||
|
||||
if not result:
|
||||
logging.error("Import returned None (Project not found or GTM DB issue)")
|
||||
return {"error": "GTM Project not found or import failed"}
|
||||
|
||||
logging.info(f"Successfully imported Project. ID: {result.get('id')}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.error(f"Exception during import: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
def list_content_projects(payload):
|
||||
projects = db_manager.get_all_content_projects()
|
||||
return {"projects": projects}
|
||||
logging.info("Executing list_content_projects")
|
||||
try:
|
||||
projects = db_manager.get_all_content_projects()
|
||||
logging.debug(f"Found {len(projects)} Content projects")
|
||||
return {"projects": projects}
|
||||
except Exception as e:
|
||||
logging.error(f"Error listing Content projects: {e}", exc_info=True)
|
||||
return {"error": str(e), "projects": []}
|
||||
|
||||
def load_project_details(payload):
|
||||
project_id = payload.get('projectId')
|
||||
project = db_manager.get_content_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Project not found"}
|
||||
logging.info(f"Loading project details for ID: {project_id}")
|
||||
|
||||
assets = db_manager.get_project_assets(project_id)
|
||||
project['assets'] = assets
|
||||
return project
|
||||
try:
|
||||
project = db_manager.get_content_project(project_id)
|
||||
if not project:
|
||||
logging.warning(f"Project ID {project_id} not found")
|
||||
return {"error": "Project not found"}
|
||||
|
||||
assets = db_manager.get_project_assets(project_id)
|
||||
project['assets'] = assets
|
||||
logging.info(f"Loaded project '{project.get('name')}' with {len(assets)} assets.")
|
||||
return project
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading project: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
def seo_brainstorming(payload):
|
||||
project_id = payload.get('projectId')
|
||||
lang = payload.get('lang', 'de')
|
||||
logging.info(f"Starting SEO Brainstorming for Project ID: {project_id}")
|
||||
|
||||
project = db_manager.get_content_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Project context not found"}
|
||||
|
||||
gtm_data = project.get('gtm_data_snapshot', {})
|
||||
|
||||
# GOLDEN RULE: Use Raw Quotes and .format()
|
||||
prompt = r"""
|
||||
Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt):
|
||||
{gtm_context}
|
||||
|
||||
AUFGABE:
|
||||
Generiere eine strategische Liste von 15 SEO-Keywords.
|
||||
1. 5 Short-Tail Fokus-Keywords (z.B. Produktkategorie + 'kaufen/mieten').
|
||||
2. 10 Long-Tail Keywords, die spezifische Pain Points oder Usecases adressieren (z.B. 'Kostenreduktion bei Sicherheitsrundgängen').
|
||||
|
||||
Die Keywords müssen für Entscheider relevant sein (CFO, Head of Security, Operations Manager).
|
||||
|
||||
Output NUR als JSON Liste von Strings.
|
||||
""".format(gtm_context=json.dumps(gtm_data))
|
||||
|
||||
response = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=True)
|
||||
keywords = json.loads(response)
|
||||
|
||||
db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords})
|
||||
return {"keywords": keywords}
|
||||
try:
|
||||
project = db_manager.get_content_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Project context not found"}
|
||||
|
||||
gtm_data = project.get('gtm_data_snapshot', {})
|
||||
logging.debug(f"Loaded GTM context snapshot size: {len(str(gtm_data))} chars")
|
||||
|
||||
prompt = r"""
|
||||
Basierend auf folgendem GTM-Kontext (Strategie für ein technisches Produkt):
|
||||
{gtm_context}
|
||||
|
||||
AUFGABE:
|
||||
Generiere eine strategische Liste von 15 SEO-Keywords.
|
||||
1. 5 Short-Tail Fokus-Keywords (z.B. Produktkategorie + 'kaufen/mieten').
|
||||
2. 10 Long-Tail Keywords, die spezifische Pain Points oder Usecases adressieren (z.B. 'Kostenreduktion bei Sicherheitsrundgängen').
|
||||
|
||||
Die Keywords müssen für Entscheider relevant sein (CFO, Head of Security, Operations Manager).
|
||||
|
||||
Output NUR als JSON Liste von Strings.
|
||||
""".format(gtm_context=json.dumps(gtm_data))
|
||||
|
||||
logging.info("Calling Gemini Flash for keywords...")
|
||||
response = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=True)
|
||||
logging.info("Gemini response received.")
|
||||
|
||||
keywords = json.loads(response)
|
||||
|
||||
db_manager.save_seo_strategy(project_id, {"seed_keywords": keywords})
|
||||
logging.info("Keywords saved to DB.")
|
||||
return {"keywords": keywords}
|
||||
except Exception as e:
|
||||
logging.error(f"Error in SEO Brainstorming: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
def generate_section(payload):
|
||||
project_id = payload.get('projectId')
|
||||
section_key = payload.get('sectionKey') # e.g., 'hero', 'problem', 'features'
|
||||
section_key = payload.get('sectionKey')
|
||||
manual_content = payload.get('manualContent')
|
||||
lang = payload.get('lang', 'de')
|
||||
keywords = payload.get('keywords', [])
|
||||
|
||||
if manual_content:
|
||||
# User is saving their manual edits
|
||||
db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", manual_content, keywords)
|
||||
return {"status": "saved", "sectionKey": section_key}
|
||||
logging.info(f"Generating section '{section_key}' for Project {project_id}")
|
||||
|
||||
project = db_manager.get_content_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Project context not found"}
|
||||
if manual_content:
|
||||
logging.info("Saving manual content update.")
|
||||
try:
|
||||
db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", manual_content, keywords)
|
||||
return {"status": "saved", "sectionKey": section_key}
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving manual content: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
try:
|
||||
project = db_manager.get_content_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Project context not found"}
|
||||
|
||||
gtm_data = project.get('gtm_data_snapshot', {})
|
||||
category = project.get('category')
|
||||
|
||||
gtm_data = project.get('gtm_data_snapshot', {})
|
||||
|
||||
# Context extraction
|
||||
category = project.get('category')
|
||||
|
||||
prompt = r"""
|
||||
Erstelle den Website-Inhalt für die Sektion '{section}' eines Produkts in der Kategorie '{cat}'.
|
||||
|
||||
STRATEGIE-KONTEXT:
|
||||
{gtm_context}
|
||||
|
||||
SEO-KEYWORDS ZU NUTZEN:
|
||||
{kws}
|
||||
|
||||
ANFORDERUNG:
|
||||
- Schreibe im Stil eines Senior Copywriters (fachlich fundiert, Challenger Sale).
|
||||
- Format: Markdown.
|
||||
- Die Sektion muss den Nutzer zur nächsten Aktion (CTA) führen.
|
||||
""".format(
|
||||
section=section_key,
|
||||
cat=category,
|
||||
gtm_context=json.dumps(gtm_data),
|
||||
kws=json.dumps(keywords)
|
||||
)
|
||||
|
||||
content = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=False)
|
||||
|
||||
# Save as asset
|
||||
db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords)
|
||||
|
||||
return {"content": content, "sectionKey": section_key}
|
||||
prompt = r"""
|
||||
Erstelle den Website-Inhalt für die Sektion '{section}' eines Produkts in der Kategorie '{cat}'.
|
||||
|
||||
STRATEGIE-KONTEXT:
|
||||
{gtm_context}
|
||||
|
||||
SEO-KEYWORDS ZU NUTZEN:
|
||||
{kws}
|
||||
|
||||
ANFORDERUNG:
|
||||
- Schreibe im Stil eines Senior Copywriters (fachlich fundiert, Challenger Sale).
|
||||
- Format: Markdown.
|
||||
- Die Sektion muss den Nutzer zur nächsten Aktion (CTA) führen.
|
||||
""".format(
|
||||
section=section_key,
|
||||
cat=category,
|
||||
gtm_context=json.dumps(gtm_data),
|
||||
kws=json.dumps(keywords)
|
||||
)
|
||||
|
||||
logging.info("Calling Gemini for copy generation...")
|
||||
content = call_gemini_flash(prompt, system_instruction=get_copywriter_instruction(lang), json_mode=False)
|
||||
logging.info("Copy generated.")
|
||||
|
||||
db_manager.save_content_asset(project_id, 'website_section', section_key, f"Section: {section_key}", content, keywords)
|
||||
|
||||
return {"content": content, "sectionKey": section_key}
|
||||
except Exception as e:
|
||||
logging.error(f"Error generating section: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Content Engine Orchestrator")
|
||||
@@ -152,11 +220,18 @@ def main():
|
||||
parser.add_argument("--payload_file", help="Path to JSON payload")
|
||||
|
||||
args = parser.parse_args()
|
||||
logging.info(f"Orchestrator called with mode: {args.mode}")
|
||||
|
||||
payload = {}
|
||||
if args.payload_file:
|
||||
with open(args.payload_file, 'r') as f:
|
||||
payload = json.load(f)
|
||||
try:
|
||||
with open(args.payload_file, 'r') as f:
|
||||
payload = json.load(f)
|
||||
logging.debug("Payload loaded successfully.")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load payload file: {e}")
|
||||
print(json.dumps({"error": "Failed to load payload"}))
|
||||
sys.exit(1)
|
||||
|
||||
modes = {
|
||||
"list_gtm_projects": list_gtm_projects,
|
||||
@@ -170,12 +245,14 @@ def main():
|
||||
if args.mode in modes:
|
||||
try:
|
||||
result = modes[args.mode](payload)
|
||||
# DUMP RESULT TO STDOUT
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
except Exception as e:
|
||||
logging.error(f"Error in mode {args.mode}: {str(e)}")
|
||||
print(json.dumps({"error": str(e)}))
|
||||
logging.critical(f"Unhandled exception in mode execution: {e}", exc_info=True)
|
||||
print(json.dumps({"error": f"Internal Error: {str(e)}"}))
|
||||
else:
|
||||
logging.error(f"Unknown mode: {args.mode}")
|
||||
print(json.dumps({"error": f"Unknown mode: {args.mode}"}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Reference in New Issue
Block a user