Add autonomous press release pipeline tool
Implements write_press_releases tool that generates 7 headlines via chat brain, AI-judges the best 2, writes 2 full press releases via execution brain, and generates JSON-LD schemas via Sonnet with WebSearch. Saves all output files to data/generated/press_releases/. Also adds tools/model pass-through in agent and LLM layers, fixes Windows command line length limit by piping prompts via stdin, and updates model references to current versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>cora-start
parent
1866d48cb2
commit
b3140d3522
|
|
@ -153,14 +153,29 @@ class Agent:
|
|||
result_parts.append(chunk)
|
||||
return "".join(result_parts)
|
||||
|
||||
def execute_task(self, prompt: str, system_context: str = "") -> str:
|
||||
def execute_task(
|
||||
self,
|
||||
prompt: str,
|
||||
system_context: str = "",
|
||||
tools: str = "",
|
||||
model: str = "",
|
||||
) -> str:
|
||||
"""Execute a task using the execution brain (Claude Code CLI).
|
||||
|
||||
Used by heartbeat, scheduler, and the delegate tool.
|
||||
Logs the result to daily memory if available.
|
||||
|
||||
Args:
|
||||
tools: Override Claude Code tool list (e.g. "Bash,Read,WebSearch").
|
||||
model: Override the CLI model (e.g. "claude-sonnet-4.5").
|
||||
"""
|
||||
log.info("Execution brain task: %s", prompt[:100])
|
||||
result = self.llm.execute(prompt, system_prompt=system_context)
|
||||
kwargs: dict = {"system_prompt": system_context}
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
if model:
|
||||
kwargs["model"] = model
|
||||
result = self.llm.execute(prompt, **kwargs)
|
||||
|
||||
# Log to daily memory
|
||||
if self._memory:
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ShellConfig:
|
|||
@dataclass
|
||||
class Config:
|
||||
chat_model: str = "openai/gpt-4o-mini"
|
||||
default_model: str = "claude-sonnet-4-20250514"
|
||||
default_model: str = "claude-sonnet-4.5"
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 7860
|
||||
ollama_url: str = "http://localhost:11434"
|
||||
|
|
|
|||
|
|
@ -37,10 +37,9 @@ class ModelInfo:
|
|||
|
||||
# Claude model IDs → OpenRouter equivalents (for chat dropdown)
|
||||
CLAUDE_OPENROUTER_MAP = {
|
||||
"claude-sonnet-4-20250514": "anthropic/claude-sonnet-4.5",
|
||||
"claude-sonnet-4.5": "anthropic/claude-sonnet-4.5",
|
||||
"claude-opus-4-20250514": "anthropic/claude-opus-4.6",
|
||||
"claude-opus-4.6": "anthropic/claude-opus-4.6",
|
||||
"claude-haiku-4.5": "anthropic/claude-haiku-4.5",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -57,7 +56,7 @@ def _provider_for(model_id: str, openrouter_key: str) -> str:
|
|||
class LLMAdapter:
|
||||
def __init__(
|
||||
self,
|
||||
default_model: str = "claude-sonnet-4-20250514",
|
||||
default_model: str = "claude-sonnet-4.5",
|
||||
openrouter_key: str = "",
|
||||
ollama_url: str = "http://localhost:11434",
|
||||
lmstudio_url: str = "http://localhost:1234",
|
||||
|
|
@ -125,21 +124,30 @@ class LLMAdapter:
|
|||
prompt: str,
|
||||
system_prompt: str = "",
|
||||
working_dir: str | None = None,
|
||||
tools: str = "Bash,Read,Edit,Write,Glob,Grep",
|
||||
model: str | None = None,
|
||||
) -> str:
|
||||
"""Execution brain: calls Claude Code CLI with full tool access.
|
||||
|
||||
Used for heartbeat checks, scheduled tasks, and delegated complex tasks.
|
||||
Returns the full result string (non-streaming).
|
||||
|
||||
Args:
|
||||
tools: Comma-separated Claude Code tool names (default: standard set).
|
||||
model: Override the CLI model (e.g. "claude-sonnet-4.5").
|
||||
"""
|
||||
claude_bin = shutil.which("claude")
|
||||
if not claude_bin:
|
||||
return "Error: `claude` CLI not found in PATH. Install Claude Code: npm install -g @anthropic-ai/claude-code"
|
||||
|
||||
# Pipe prompt through stdin to avoid Windows 8191-char command-line limit.
|
||||
cmd = [
|
||||
claude_bin, "-p", prompt,
|
||||
claude_bin, "-p",
|
||||
"--output-format", "json",
|
||||
"--tools", "Bash,Read,Edit,Write,Glob,Grep",
|
||||
"--tools", tools,
|
||||
]
|
||||
if model:
|
||||
cmd.extend(["--model", model])
|
||||
if system_prompt:
|
||||
cmd.extend(["--system-prompt", system_prompt])
|
||||
|
||||
|
|
@ -151,6 +159,7 @@ class LLMAdapter:
|
|||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
|
|
@ -163,7 +172,7 @@ class LLMAdapter:
|
|||
return "Error: `claude` CLI not found. Install Claude Code: npm install -g @anthropic-ai/claude-code"
|
||||
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=300)
|
||||
stdout, stderr = proc.communicate(input=prompt, timeout=300)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return "Error: Claude Code execution timed out after 5 minutes."
|
||||
|
|
|
|||
|
|
@ -0,0 +1,488 @@
|
|||
"""Press-release pipeline tool.
|
||||
|
||||
Autonomous workflow:
|
||||
1. Generate 7 compliant headlines (chat brain)
|
||||
2. AI judge picks the 2 best (chat brain)
|
||||
3. Write 2 full press releases (execution brain × 2)
|
||||
4. Generate 2 JSON-LD schemas (execution brain × 2, Sonnet + WebSearch)
|
||||
5. Save 4 files, return cost summary
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from . import tool
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ROOT_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
_SKILLS_DIR = _ROOT_DIR / "skills"
|
||||
_DATA_DIR = _ROOT_DIR / "data"
|
||||
_OUTPUT_DIR = _DATA_DIR / "generated" / "press_releases"
|
||||
_COMPANIES_FILE = _SKILLS_DIR / "companies.md"
|
||||
_HEADLINES_FILE = _SKILLS_DIR / "headlines.md"
|
||||
|
||||
SONNET_CLI_MODEL = "sonnet"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_skill(filename: str) -> str:
|
||||
"""Read a markdown skill file from the skills/ directory."""
|
||||
path = _SKILLS_DIR / filename
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Skill file not found: {path}")
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _load_file_if_exists(path: Path) -> str:
|
||||
"""Read a file if it exists, return empty string otherwise."""
|
||||
if path.exists():
|
||||
return path.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
|
||||
def _slugify(text: str) -> str:
|
||||
"""Turn a headline into a filesystem-safe slug."""
|
||||
text = text.lower().strip()
|
||||
text = re.sub(r"[^\w\s-]", "", text)
|
||||
text = re.sub(r"[\s_]+", "-", text)
|
||||
return text[:60].strip("-")
|
||||
|
||||
|
||||
def _word_count(text: str) -> int:
|
||||
return len(text.split())
|
||||
|
||||
|
||||
def _chat_call(agent, messages: list[dict]) -> str:
|
||||
"""Make a non-streaming chat-brain call and return the full text."""
|
||||
parts: list[str] = []
|
||||
for chunk in agent.llm.chat(messages, tools=None, stream=False):
|
||||
if chunk["type"] == "text":
|
||||
parts.append(chunk["content"])
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _clean_pr_output(raw: str, headline: str) -> str:
|
||||
"""Clean execution brain output to just the press release text.
|
||||
|
||||
Strategy: find the headline we asked for in the output, take everything
|
||||
from that point forward. Strip any markdown formatting artifacts.
|
||||
"""
|
||||
# Normalize the headline for matching
|
||||
headline_lower = headline.strip().lower()
|
||||
|
||||
lines = raw.strip().splitlines()
|
||||
|
||||
# Try to find the exact headline in the output
|
||||
pr_start = None
|
||||
for i, line in enumerate(lines):
|
||||
clean_line = re.sub(r"\*\*", "", line).strip().lower()
|
||||
if clean_line == headline_lower:
|
||||
pr_start = i
|
||||
break
|
||||
|
||||
# Fallback: find a line that contains most of the headline words
|
||||
if pr_start is None:
|
||||
headline_words = set(headline_lower.split())
|
||||
for i, line in enumerate(lines):
|
||||
clean_line = re.sub(r"\*\*", "", line).strip().lower()
|
||||
line_words = set(clean_line.split())
|
||||
# If >70% of headline words are in this line, it's probably the headline
|
||||
if len(headline_words & line_words) >= len(headline_words) * 0.7:
|
||||
pr_start = i
|
||||
break
|
||||
|
||||
# If we still can't find it, just take the whole output
|
||||
if pr_start is None:
|
||||
pr_start = 0
|
||||
|
||||
# Rebuild from the headline forward
|
||||
result_lines = []
|
||||
for line in lines[pr_start:]:
|
||||
# Strip markdown formatting
|
||||
line = re.sub(r"\*\*", "", line)
|
||||
line = re.sub(r"^#{1,6}\s+", "", line)
|
||||
result_lines.append(line)
|
||||
|
||||
result = "\n".join(result_lines).strip()
|
||||
|
||||
# Remove trailing horizontal rules
|
||||
result = re.sub(r"\n---\s*$", "", result).strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prompt builders
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_headline_prompt(topic: str, company_name: str, url: str,
|
||||
lsi_terms: str, headlines_ref: str) -> str:
|
||||
"""Build the prompt for Step 1: generate 7 headlines."""
|
||||
prompt = (
|
||||
f"Generate exactly 7 unique press release headline options for the following.\n\n"
|
||||
f"Topic: {topic}\n"
|
||||
f"Company: {company_name}\n"
|
||||
)
|
||||
if url:
|
||||
prompt += f"Reference URL: {url}\n"
|
||||
if lsi_terms:
|
||||
prompt += f"LSI terms to consider: {lsi_terms}\n"
|
||||
|
||||
prompt += (
|
||||
"\nRules for EVERY headline:\n"
|
||||
"- Maximum 70 characters (including spaces)\n"
|
||||
"- Title case\n"
|
||||
"- News-focused, not promotional\n"
|
||||
"- NO location/geographic keywords\n"
|
||||
"- NO superlatives (best, top, leading, #1)\n"
|
||||
"- NO questions\n"
|
||||
"- NO colons — colons are considered lower quality\n"
|
||||
"- Must contain an actual news announcement\n"
|
||||
)
|
||||
|
||||
if headlines_ref:
|
||||
prompt += (
|
||||
"\nHere are examples of high-quality headlines to use as reference "
|
||||
"for tone, structure, and length:\n\n"
|
||||
f"{headlines_ref}\n"
|
||||
)
|
||||
|
||||
prompt += (
|
||||
"\nReturn ONLY a numbered list (1-7), one headline per line. "
|
||||
"No commentary, no character counts, just the headlines."
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def _build_judge_prompt(headlines: str, headlines_ref: str) -> str:
|
||||
"""Build the prompt for Step 2: pick the 2 best headlines."""
|
||||
prompt = (
|
||||
"You are judging press release headlines for Press Advantage distribution. "
|
||||
"Pick the 2 best headlines from the candidates below.\n\n"
|
||||
"DISQUALIFY any headline that:\n"
|
||||
"- Contains a colon\n"
|
||||
"- Contains location/geographic keywords\n"
|
||||
"- Contains superlatives (best, top, leading, #1)\n"
|
||||
"- Is a question\n"
|
||||
"- Exceeds 70 characters\n"
|
||||
"- Implies a NEW product launch when none exists (avoid 'launches', "
|
||||
"'introduces', 'unveils', 'announces new' unless the topic is genuinely new)\n\n"
|
||||
"PREFER headlines that:\n"
|
||||
"- Match the tone and structure of the reference examples below\n"
|
||||
"- Use action verbs like 'Highlights', 'Expands', 'Strengthens', "
|
||||
"'Reinforces', 'Delivers', 'Adds'\n"
|
||||
"- Describe what the company DOES or OFFERS, not what it just invented\n"
|
||||
"- Read like a real news wire headline, not a product announcement\n\n"
|
||||
f"Candidates:\n{headlines}\n\n"
|
||||
)
|
||||
|
||||
if headlines_ref:
|
||||
prompt += (
|
||||
"Reference headlines (these scored 77+ on quality — match their style):\n"
|
||||
f"{headlines_ref}\n\n"
|
||||
)
|
||||
|
||||
prompt += (
|
||||
"Return ONLY the 2 best headlines, one per line, exactly as written in the candidates. "
|
||||
"No numbering, no commentary."
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def _build_pr_prompt(headline: str, topic: str, company_name: str,
|
||||
url: str, lsi_terms: str, required_phrase: str,
|
||||
skill_text: str, companies_file: str) -> str:
|
||||
"""Build the prompt for Step 3: write one full press release."""
|
||||
prompt = (
|
||||
f"{skill_text}\n\n"
|
||||
"---\n\n"
|
||||
f"Write a press release using the headline below. "
|
||||
f"Follow every rule in the skill instructions above.\n\n"
|
||||
f"Headline: {headline}\n"
|
||||
f"Topic: {topic}\n"
|
||||
f"Company: {company_name}\n"
|
||||
)
|
||||
if url:
|
||||
prompt += f"Reference URL (fetch for context): {url}\n"
|
||||
if lsi_terms:
|
||||
prompt += f"LSI terms to integrate: {lsi_terms}\n"
|
||||
if required_phrase:
|
||||
prompt += f'Required phrase (use exactly once): "{required_phrase}"\n'
|
||||
|
||||
if companies_file:
|
||||
prompt += (
|
||||
f"\nCompany directory — look up the executive name and title for {company_name}. "
|
||||
f"If the company is NOT listed below, use 'a company spokesperson' for quotes "
|
||||
f"instead of making up a name:\n"
|
||||
f"{companies_file}\n"
|
||||
)
|
||||
|
||||
prompt += (
|
||||
"\nTarget 600-750 words. Minimum 575, maximum 800.\n\n"
|
||||
"CRITICAL OUTPUT RULES:\n"
|
||||
"- Output ONLY the press release text\n"
|
||||
"- Start with the headline on the first line, then the body\n"
|
||||
"- Do NOT include any commentary, reasoning, notes, or explanations\n"
|
||||
"- Do NOT use markdown formatting (no **, no ##, no ---)\n"
|
||||
"- Do NOT prefix with 'Here is the press release' or similar\n"
|
||||
"- The very first line of your output must be the headline"
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def _build_schema_prompt(pr_text: str, company_name: str, url: str,
|
||||
skill_text: str) -> str:
|
||||
"""Build the prompt for Step 4: generate JSON-LD schema for one PR."""
|
||||
prompt = (
|
||||
f"{skill_text}\n\n"
|
||||
"---\n\n"
|
||||
"Generate a NewsArticle JSON-LD schema for the press release below. "
|
||||
"Follow every rule in the skill instructions above. "
|
||||
"Use WebSearch to find Wikipedia URLs for each entity.\n\n"
|
||||
"CRITICAL OUTPUT RULES:\n"
|
||||
"- Output ONLY valid JSON\n"
|
||||
"- No markdown fences, no commentary, no explanations\n"
|
||||
"- The very first character of your output must be {\n"
|
||||
)
|
||||
prompt += (
|
||||
f"\nCompany name: {company_name}\n\n"
|
||||
f"Press release text:\n{pr_text}"
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main tool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tool(
|
||||
"write_press_releases",
|
||||
description=(
|
||||
"Full autonomous press-release pipeline. Generates 7 headlines, "
|
||||
"AI-picks the best 2, writes 2 complete press releases (600-750 words each), "
|
||||
"generates JSON-LD schema for each, and saves all files. "
|
||||
"Returns both press releases, both schemas, file paths, and a cost summary. "
|
||||
"Use when the user asks to write, create, or draft a press release."
|
||||
),
|
||||
category="content",
|
||||
)
|
||||
def write_press_releases(
|
||||
topic: str,
|
||||
company_name: str,
|
||||
url: str = "",
|
||||
lsi_terms: str = "",
|
||||
required_phrase: str = "",
|
||||
ctx: dict = None,
|
||||
) -> str:
|
||||
"""Run the full press-release pipeline and return results + cost summary."""
|
||||
if not ctx or "agent" not in ctx:
|
||||
return "Error: press release tool requires agent context."
|
||||
|
||||
agent = ctx["agent"]
|
||||
|
||||
# Load skill prompts
|
||||
try:
|
||||
pr_skill = _load_skill("press_release_prompt.md")
|
||||
schema_skill = _load_skill("press-release-schema.md")
|
||||
except FileNotFoundError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
# Load reference files
|
||||
companies_file = _load_file_if_exists(_COMPANIES_FILE)
|
||||
headlines_ref = _load_file_if_exists(_HEADLINES_FILE)
|
||||
|
||||
# Ensure output directory
|
||||
_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
cost_log: list[dict] = []
|
||||
|
||||
# ── Step 1: Generate 7 headlines (chat brain) ─────────────────────────
|
||||
step_start = time.time()
|
||||
headline_prompt = _build_headline_prompt(topic, company_name, url, lsi_terms, headlines_ref)
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a senior press-release headline writer."},
|
||||
{"role": "user", "content": headline_prompt},
|
||||
]
|
||||
headlines_raw = _chat_call(agent, messages)
|
||||
cost_log.append({
|
||||
"step": "1. Generate 7 headlines",
|
||||
"model": agent.llm.current_model,
|
||||
"elapsed_s": round(time.time() - step_start, 1),
|
||||
})
|
||||
|
||||
if not headlines_raw.strip():
|
||||
return "Error: headline generation returned empty result."
|
||||
|
||||
# Save all 7 headline candidates to file
|
||||
slug_base = _slugify(f"{company_name}-{topic}")
|
||||
headlines_file = _OUTPUT_DIR / f"{slug_base}_{today}_headlines.txt"
|
||||
headlines_file.write_text(headlines_raw.strip(), encoding="utf-8")
|
||||
|
||||
# ── Step 2: AI judge picks best 2 (chat brain) ───────────────────────
|
||||
step_start = time.time()
|
||||
judge_prompt = _build_judge_prompt(headlines_raw, headlines_ref)
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a senior PR editor."},
|
||||
{"role": "user", "content": judge_prompt},
|
||||
]
|
||||
judge_result = _chat_call(agent, messages)
|
||||
cost_log.append({
|
||||
"step": "2. Judge picks best 2",
|
||||
"model": agent.llm.current_model,
|
||||
"elapsed_s": round(time.time() - step_start, 1),
|
||||
})
|
||||
|
||||
# Parse the two winning headlines
|
||||
winners = [line.strip().lstrip("0123456789.-) ") for line in judge_result.strip().splitlines() if line.strip()]
|
||||
if len(winners) < 2:
|
||||
all_headlines = [line.strip().lstrip("0123456789.-) ") for line in headlines_raw.strip().splitlines() if line.strip()]
|
||||
winners = all_headlines[:2] if len(all_headlines) >= 2 else [all_headlines[0], all_headlines[0]] if all_headlines else ["Headline A", "Headline B"]
|
||||
winners = winners[:2]
|
||||
|
||||
# ── Step 3: Write 2 press releases (execution brain × 2) ─────────────
|
||||
pr_texts: list[str] = []
|
||||
pr_files: list[str] = []
|
||||
for i, headline in enumerate(winners):
|
||||
step_start = time.time()
|
||||
pr_prompt = _build_pr_prompt(
|
||||
headline, topic, company_name, url, lsi_terms,
|
||||
required_phrase, pr_skill, companies_file,
|
||||
)
|
||||
exec_tools = "Bash,Read,Edit,Write,Glob,Grep,WebFetch"
|
||||
raw_result = agent.execute_task(pr_prompt, tools=exec_tools)
|
||||
elapsed = round(time.time() - step_start, 1)
|
||||
cost_log.append({
|
||||
"step": f"3{chr(97+i)}. Write PR '{headline[:40]}...'",
|
||||
"model": "execution-brain (default)",
|
||||
"elapsed_s": elapsed,
|
||||
})
|
||||
|
||||
# Clean output: find the headline, strip preamble and markdown
|
||||
clean_result = _clean_pr_output(raw_result, headline)
|
||||
pr_texts.append(clean_result)
|
||||
|
||||
# Validate word count
|
||||
wc = _word_count(clean_result)
|
||||
if wc < 575 or wc > 800:
|
||||
log.warning("PR %d word count %d outside 575-800 range", i + 1, wc)
|
||||
|
||||
# Save PR to file
|
||||
slug = _slugify(headline)
|
||||
filename = f"{slug}_{today}.txt"
|
||||
filepath = _OUTPUT_DIR / filename
|
||||
filepath.write_text(clean_result, encoding="utf-8")
|
||||
pr_files.append(str(filepath))
|
||||
|
||||
# ── Step 4: Generate 2 JSON-LD schemas (Sonnet + WebSearch) ───────────
|
||||
schema_texts: list[str] = []
|
||||
schema_files: list[str] = []
|
||||
for i, pr_text in enumerate(pr_texts):
|
||||
step_start = time.time()
|
||||
schema_prompt = _build_schema_prompt(pr_text, company_name, url, schema_skill)
|
||||
exec_tools = "WebSearch,WebFetch"
|
||||
result = agent.execute_task(
|
||||
schema_prompt,
|
||||
tools=exec_tools,
|
||||
model=SONNET_CLI_MODEL,
|
||||
)
|
||||
elapsed = round(time.time() - step_start, 1)
|
||||
cost_log.append({
|
||||
"step": f"4{chr(97+i)}. Schema for PR {i+1}",
|
||||
"model": SONNET_CLI_MODEL,
|
||||
"elapsed_s": elapsed,
|
||||
})
|
||||
|
||||
# Extract clean JSON and force correct mainEntityOfPage
|
||||
schema_json = _extract_json(result)
|
||||
if schema_json:
|
||||
try:
|
||||
schema_obj = json.loads(schema_json)
|
||||
if url:
|
||||
schema_obj["mainEntityOfPage"] = url
|
||||
schema_json = json.dumps(schema_obj, indent=2)
|
||||
except json.JSONDecodeError:
|
||||
log.warning("Schema %d is not valid JSON", i + 1)
|
||||
schema_texts.append(schema_json or result)
|
||||
|
||||
# Save schema to file
|
||||
slug = _slugify(winners[i])
|
||||
filename = f"{slug}_{today}_schema.json"
|
||||
filepath = _OUTPUT_DIR / filename
|
||||
filepath.write_text(schema_json or result, encoding="utf-8")
|
||||
schema_files.append(str(filepath))
|
||||
|
||||
# ── Build final output ────────────────────────────────────────────────
|
||||
total_elapsed = sum(c["elapsed_s"] for c in cost_log)
|
||||
output_parts = []
|
||||
|
||||
for i in range(2):
|
||||
label = chr(65 + i) # A, B
|
||||
wc = _word_count(pr_texts[i])
|
||||
output_parts.append(f"## Press Release {label}: {winners[i]}")
|
||||
output_parts.append(f"**Word count:** {wc}")
|
||||
output_parts.append(f"**File:** `{pr_files[i]}`\n")
|
||||
output_parts.append(pr_texts[i])
|
||||
output_parts.append("\n---\n")
|
||||
output_parts.append(f"### Schema {label}")
|
||||
output_parts.append(f"**File:** `{schema_files[i]}`\n")
|
||||
output_parts.append(f"```json\n{schema_texts[i]}\n```")
|
||||
output_parts.append("\n---\n")
|
||||
|
||||
# Cost summary table
|
||||
output_parts.append("## Cost Summary\n")
|
||||
output_parts.append("| Step | Model | Time (s) |")
|
||||
output_parts.append("|------|-------|----------|")
|
||||
for c in cost_log:
|
||||
output_parts.append(f"| {c['step']} | {c['model']} | {c['elapsed_s']} |")
|
||||
output_parts.append(f"| **Total** | | **{round(total_elapsed, 1)}** |")
|
||||
|
||||
return "\n".join(output_parts)
|
||||
|
||||
|
||||
def _extract_json(text: str) -> str | None:
|
||||
"""Try to pull a JSON object out of LLM output (strip fences, prose, etc)."""
|
||||
stripped = text.strip()
|
||||
if stripped.startswith("{"):
|
||||
try:
|
||||
json.loads(stripped)
|
||||
return stripped
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Strip markdown fences
|
||||
fence_match = re.search(r"```(?:json)?\s*\n?([\s\S]*?)\n?```", text)
|
||||
if fence_match:
|
||||
candidate = fence_match.group(1).strip()
|
||||
try:
|
||||
json.loads(candidate)
|
||||
return candidate
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Last resort: find first { to last }
|
||||
start = text.find("{")
|
||||
end = text.rfind("}")
|
||||
if start != -1 and end != -1 and end > start:
|
||||
candidate = text[start:end + 1]
|
||||
try:
|
||||
json.loads(candidate)
|
||||
return candidate
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
chat_model: "openai/gpt-4o-mini"
|
||||
|
||||
# Execution model (Claude Code CLI - uses Max subscription for heartbeat/scheduler)
|
||||
default_model: "claude-sonnet-4-20250514"
|
||||
default_model: "claude-sonnet-4.5"
|
||||
|
||||
# Gradio server settings
|
||||
host: "0.0.0.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
# Company Directory
|
||||
|
||||
## McCormick Industries
|
||||
- **Executive:** Gary Hermsen, CEO
|
||||
|
||||
## MCM Composites
|
||||
- **Executive:** Michael Fredrich, CEO
|
||||
|
||||
## AGI Fabricators
|
||||
- **Executive:** Brad Landry, General Manager
|
||||
|
||||
## Dietz Electric
|
||||
- **Executive:** Mark Henson, Owner
|
||||
|
||||
## Metal Craft
|
||||
- **Executive:** Kyle, Vice President
|
||||
|
||||
## GullCo
|
||||
- **Executive:** Jeff Zook, Director
|
||||
|
||||
## MOD-TRONIC Instruments Limited
|
||||
- **Executive:** Steven Ruple, President
|
||||
|
||||
## Krueger Sentry Gauge
|
||||
- **Executive:** Lee Geurts, Vice President
|
||||
|
||||
## Chapter 2 Incorporated
|
||||
- **Executive:** Kyle Johnston, Senior Engineer
|
||||
|
||||
## Nicolet Plastics LLC
|
||||
- **Executive:** Brian Torres, Chief Commercial Officer
|
||||
|
||||
## Renown Electric Motors & Repairs Inc.
|
||||
- **Executive:** Jeff Collins, Partner
|
||||
|
||||
## RPM Mechanical Inc.
|
||||
- **Executive:** Mike McNeil, Vice President
|
||||
|
||||
## Green Bay Plastics
|
||||
- **Executive:** Michael Hogan, President
|
||||
|
||||
## Paragon Steel
|
||||
- **Executive:** Jim Stavis, President & CEO
|
||||
|
||||
## Hogge Precision
|
||||
- **Executive:** Danny Hogge Jr, President
|
||||
|
||||
## Axiomatic Global Electronic Solutions
|
||||
- **Executive:** Amanda Wilkins, Chief Marketing Officer
|
||||
|
||||
## Advanced Industrial
|
||||
- **Executive:** Paul Cedrone, CEO
|
||||
|
||||
## ELIS Manufacturing and Packaging Solutions Inc.
|
||||
- **Executive:** Keith Vinson, Chief Executive Officer
|
||||
|
||||
## Lubrication Engineers
|
||||
- **Executive:** John Sander, Vice President of Research & Development
|
||||
|
||||
## FZE Industrial
|
||||
- **Executive:** Doug Pribyl, CEO
|
||||
|
||||
## Machine Specialty & Manufacturing (MSM)
|
||||
- **Executive:** Max Hutson, Vice President of Operations
|
||||
|
||||
## DCA
|
||||
- **Executive:** Errol Gelhaar (title unknown)
|
||||
|
||||
## EVR Products
|
||||
|
||||
- **Executive:** Gary Waldick, Vice President of EVR Products
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Good Press Release Headlines
|
||||
|
||||
These are real headlines that scored above 77 on quality. Use them as reference for tone, structure, and length.
|
||||
|
||||
**Note:** Headlines with colons (:) are considered lower quality and should be avoided.
|
||||
|
||||
Dietz Electric Highlights Flameproof Motor Safety Options
|
||||
MOD-TRONIC Reaffirms Position as Largest MINCO Stocking Distributor
|
||||
Hogge Precision Parts Delivers Precision Machining for the Medical Industry
|
||||
Lubrication Engineers Drives Awareness of Fuel Treatment Benefits for Year-Round Fleet Efficiency
|
||||
MCM Composites Releases Enhanced Thermoset Comparison Resource
|
||||
Renown Electric Champions Proactive Downtime Protection With Contingency Planning Insights
|
||||
Dehumidifier Corporation of America Introduces New Digital Assistant for Technical Support
|
||||
Dehumidifier Corporation of America Strengthens Support for Controlled-Environment Agriculture with Enhanced Grow Room Dehumidifier Line
|
||||
Chapter 2 Strengthens Production Capacity with Installation of Horizontal Machining System
|
||||
Lubrication Engineers Highlights the Role of Hydraulic Oils in Industrial Reliability
|
||||
AGI Fabricators Publishes New Resource on Custom Process Hopper Fabrication
|
||||
Paragon Steel Enhances Delivery Options for South Bay Industrial Projects
|
||||
Paragon Steel Bolsters Capabilities for Port-Area Infrastructure Work
|
||||
Paragon Steel Expands Support for Westside Tech and Industrial Projects
|
||||
Industrial Marketing Specialists Announces Strategic Marketing Partnership With EVR Products
|
||||
Paragon Steel Strengthens Support For Central Los Angeles Commercial Projects
|
||||
AGI Fabricators Expands Access to Custom Dust Collectors Amid Growing Demand for Filtration Equipment
|
||||
Nicolet Plastics Adds Advanced Two-Shot Injection Molding Equipment to Operations
|
||||
FZE Manufacturing Highlights CNC HMC and VMC Machining Capabilities for Production Efficiency
|
||||
ELIS Manufacturing and Packaging Solutions Highlights Expanded Stick Pack Manufacturing Capabilities
|
||||
McCormick Industries Reinforces Quality Standards With ISO 9001:2015-Certified Medical Machining
|
||||
Krueger Sentry Gauge Announces Launch of KSG Smart Gauge Transmitter for Remote Tank Monitoring
|
||||
MOD-TRONIC Underscores MT300's Role in Advancing Temperature Monitoring Standards
|
||||
RPM Announces Availability of Plate Compactor Rubber Mounts
|
||||
FZE Manufacturing Highlights Five Decades of Leadership in Hydraulic Parts Machining Solutions
|
||||
Machine Specialty & Manufacturing Announces Available Capacity For Nondestructive Testing Services
|
||||
ELIS Manufacturing & Packaging Reinvents Powder Production with Tailored Custom Blending Solutions
|
||||
Machine Specialty & Manufacturing Reinforces Commitment to Quality and Safety in Welding and API Flange Solutions
|
||||
McCormick Industries Strengthens Aerospace Machining Operations with Next-Generation CNC Technology
|
||||
Dietz Electric Strengthens Industrial Support with Custom Motor Builds Tailored to Extreme Conditions and Unique Applications
|
||||
Hogge Precision's Methodical Innovation Recognized Among South Carolina's Elite Manufacturers
|
||||
MCM Composites Showcases How Thermoset Molding Transforms Aerospace, Appliance & Electronics Manufacturing
|
||||
Renown Electric Highlights Advances in Industrial Motor Vibration Analysis Services
|
||||
Machine Specialty & Manufacturing Announces Blind Flange Production Capability to Meet Growing Industry Demand
|
||||
Nicolet Plastics Launches Comprehensive Guide to Multi-Cavity Molding Techniques Boosting Efficiency in Mass Production
|
||||
Dietz Electric Highlights Critical Role of Hazardous Location Motors In Workplace Safety
|
||||
McCormick Industries Advances CNC Machining Technology for Quality Stainless Steel Components
|
||||
Hogge Precision Delivers Lightweight, High-Precision Aluminum Manifolds for Mobile & Industrial Equipment
|
||||
FZE Manufacturing Announces Official YouTube Channel to Expand Educational Resources and Industry Insights
|
||||
Advanced Industrial Highlights PTFE Product Line to Meet Growing Demand for High-Performance Materials
|
||||
Chapter 2 Incorporated Delivers Advanced Custom Machine Building and Retrofitting Solutions to Global Manufacturing Clients
|
||||
MCM Composites Reaches 42-Year Milestone as Leading ISO-Certified Thermoset Molding Specialist
|
||||
Metal Craft Spinning & Stamping Expands Custom Metal Solutions with Automated Manufacturing Capabilities
|
||||
Thermoset Insert Molding Capabilities at MCM Composites Offer Enhanced Strength and Durability
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
---
|
||||
name: press-release-schema
|
||||
description: Generate valid NewsArticle JSON-LD schema markup for press releases with proper entity linking to Wikipedia. UPDATED VERSION with fixed entity identification. Use when the user asks to create, generate, or add schema markup, structured data, JSON-LD, or SEO schema for a press release, news article, or announcement.
|
||||
---
|
||||
|
||||
# Press Release Schema Generator v2
|
||||
|
||||
## Overview
|
||||
|
||||
Generate high-quality NewsArticle JSON-LD schema markup for press releases following Schema.org specifications. This skill automates the creation of structured data that helps search engines understand press release content, including entity recognition and Wikipedia URL linking for improved SEO.
|
||||
|
||||
## Input Requirements
|
||||
|
||||
The press release text contains all necessary information. Extract:
|
||||
|
||||
1. **Company Name** - Organization publishing the press release (extract from text)
|
||||
2. **Target URL** - Extract from the "For more information" or similar link in the press release (use as mainEntityOfPage)
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Extract Core Metadata
|
||||
|
||||
From the press release text, identify:
|
||||
|
||||
- **Headline**: Extract the main title/headline from the text
|
||||
- **Description**: Write a concise 1-sentence summary that captures the key message
|
||||
- **IMPORTANT**: This is a summary, NOT the entire press release text
|
||||
- Should be 15-25 words maximum
|
||||
- Focus on the main announcement or key takeaway
|
||||
|
||||
### Step 2: Identify Entities with Wikipedia URLs
|
||||
|
||||
**CRITICAL - Follow this order:**
|
||||
|
||||
**Phase 1: Named Entities (ALWAYS START HERE)**
|
||||
First, systematically identify ALL proper nouns mentioned in the press release:
|
||||
- **Companies/Organizations**: Any company, manufacturer, or organization mentioned (e.g., the company issuing the release, partners, suppliers)
|
||||
- **Products/Brands**: Specific product names, brand names, or proprietary technologies
|
||||
- **People**: Names of executives, spokespeople, or other individuals quoted or mentioned
|
||||
|
||||
Search for Wikipedia URLs for EACH of these named entities. These are your primary "about" entities.
|
||||
|
||||
**Phase 2: Topical/Technical Entities (ONLY AFTER PHASE 1)**
|
||||
After identifying all named entities, then identify topical subjects:
|
||||
|
||||
**"about" entities** are what the press release is fundamentally announcing:
|
||||
- The core announcement (e.g., partnership, product launch, expansion, award, appointment)
|
||||
- Main relationships or business models being discussed (e.g., distribution, licensing, acquisition)
|
||||
- Primary named entities directly involved in the announcement
|
||||
- Example: A press release announcing a distribution partnership would have the manufacturer, distributor, and "distribution" as "about" entities
|
||||
- **Be comprehensive, not restrictive**: If it's mentioned and relevant to the announcement, include it. A press release can be "about" multiple things.
|
||||
|
||||
**"mentions" entities** are supporting context:
|
||||
- Technical concepts, technologies, or products referenced
|
||||
- Industry sectors or applications discussed
|
||||
- People quoted or mentioned
|
||||
- Secondary topics that provide context but aren't the main announcement
|
||||
- Example: In a distribution announcement, the specific product categories and technical applications would be "mentions"
|
||||
- **Be inclusive**: If a technology, concept, or industry is mentioned in the press release and is relevant to understanding the announcement, include it. Don't artificially limit the list - comprehensiveness is better than brevity.
|
||||
|
||||
**Key distinction**: Ask yourself "What is this press release announcing?" vs "What context is provided?" The announcement subjects go in "about," the context goes in "mentions."
|
||||
|
||||
**Wikipedia URL Requirements:**
|
||||
- Use web_search to find the correct Wikipedia URL for EACH entity
|
||||
- Search pattern: `"[entity name]" site:wikipedia.org`
|
||||
- Verify the Wikipedia page matches the context of the press release
|
||||
- For people, only include Wikipedia URL if they have a Wikipedia page; otherwise use name and jobTitle only
|
||||
- For industries and concepts, Wikipedia URLs are REQUIRED
|
||||
- **For companies and organizations:**
|
||||
- **External companies** (not issuing the press release): Include BOTH Wikipedia URL AND official website in sameAs array
|
||||
- **The issuing company** (publishing the press release): Include in "about" as Organization type, but do NOT include sameAs property since mainEntityOfPage already points to their page
|
||||
- Example: If Company A issues a press release about partnering with Company B, Company A goes in "about" without sameAs, Company B goes in "about" with sameAs to Wikipedia + their official website
|
||||
|
||||
### Step 3: Structure the JSON-LD
|
||||
|
||||
Create the schema with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "NewsArticle",
|
||||
"headline": "[extracted headline]",
|
||||
"description": "[1-sentence summary]",
|
||||
"mainEntityOfPage": "[target URL extracted from press release]",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "[company name]"
|
||||
},
|
||||
"about": [
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "[issuing company - no sameAs]"
|
||||
},
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "[external company]",
|
||||
"sameAs": [
|
||||
"[Wikipedia URL]",
|
||||
"[official website URL]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "[primary topic]",
|
||||
"sameAs": "[Wikipedia URL]"
|
||||
}
|
||||
],
|
||||
"mentions": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "[person name]",
|
||||
"jobTitle": "[job title]",
|
||||
"affiliation": "[organization]",
|
||||
"sameAs": "[Wikipedia URL if available]"
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "[industry or concept]",
|
||||
"sameAs": "[Wikipedia URL - REQUIRED]"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Rules:**
|
||||
- DO NOT include "articleBody" field
|
||||
- DO NOT include "datePublished" field
|
||||
- DO NOT include "image" field
|
||||
- Description must be a brief summary (15-25 words), NOT the entire press release text
|
||||
- Use "Thing" as @type for general entities unless a more specific type clearly applies (use Organization for companies)
|
||||
- "sameAs" can contain Wikipedia URLs and/or official company websites
|
||||
- For mentions of people without Wikipedia pages, omit "sameAs" but include name, jobTitle, and affiliation
|
||||
- For the issuing company, do NOT include sameAs (mainEntityOfPage already identifies them)
|
||||
- For external companies, include BOTH Wikipedia and official website in sameAs array
|
||||
|
||||
### Step 4: Format Output
|
||||
|
||||
Output the JSON-LD as plain JSON (do not wrap in HTML script tags):
|
||||
|
||||
**Output Guidelines:**
|
||||
- Provide ONLY the JSON content
|
||||
- No conversational filler or explanations
|
||||
- No HTML script tags
|
||||
- Ensure valid JSON formatting (proper quotes, commas, brackets)
|
||||
- Pretty-print the JSON for readability
|
||||
- Save as .json file extension
|
||||
|
||||
## Example
|
||||
|
||||
**Input:**
|
||||
```
|
||||
Text: "TechCorp Industries announced today a strategic partnership with Global AI Solutions to develop next-generation machine learning platforms. CEO Sarah Johnson stated, 'This collaboration represents a significant milestone in artificial intelligence development.' For more information, visit https://techcorp.com/news/ai-partnership"
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "NewsArticle",
|
||||
"headline": "TechCorp Industries Announces Strategic Partnership with Global AI Solutions",
|
||||
"description": "TechCorp Industries partners with Global AI Solutions to develop next-generation machine learning platforms.",
|
||||
"mainEntityOfPage": "https://techcorp.com/news/ai-partnership",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "TechCorp Industries"
|
||||
},
|
||||
"about": [
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "TechCorp Industries"
|
||||
},
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "Global AI Solutions",
|
||||
"sameAs": [
|
||||
"https://en.wikipedia.org/wiki/Global_AI_Solutions",
|
||||
"https://www.globalaisolutions.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "Strategic Partnership",
|
||||
"sameAs": "https://en.wikipedia.org/wiki/Strategic_alliance"
|
||||
}
|
||||
],
|
||||
"mentions": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "Sarah Johnson",
|
||||
"jobTitle": "CEO",
|
||||
"affiliation": "TechCorp Industries"
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "Artificial Intelligence",
|
||||
"sameAs": "https://en.wikipedia.org/wiki/Artificial_intelligence"
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "Machine Learning",
|
||||
"sameAs": "https://en.wikipedia.org/wiki/Machine_learning"
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "Technology Industry",
|
||||
"sameAs": "https://en.wikipedia.org/wiki/Technology_company"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before delivering the schema, verify:
|
||||
|
||||
- [ ] **ALL proper nouns (companies, products, people) mentioned in the press release have been searched and included if Wikipedia pages exist**
|
||||
- [ ] All Wikipedia URLs are valid and contextually appropriate
|
||||
- [ ] JSON syntax is valid (no trailing commas, proper quotes)
|
||||
- [ ] Headline accurately reflects the press release
|
||||
- [ ] Description is a brief summary (15-25 words, NOT the full press release text)
|
||||
- [ ] All relevant "about" entities identified with Wikipedia URLs
|
||||
- [ ] All mentioned industries/concepts have Wikipedia URLs
|
||||
- [ ] People entries include name, jobTitle, and affiliation
|
||||
- [ ] No "articleBody" field is included
|
||||
- [ ] No "datePublished" field is included
|
||||
- [ ] No "image" field is included
|
||||
- [ ] Output contains ONLY plain JSON (no HTML script tags or explanatory text)
|
||||
- [ ] Issuing company in "about" without sameAs
|
||||
- [ ] External companies in "about" with sameAs containing Wikipedia + official website
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
---
|
||||
name: press-release-writer
|
||||
description: Professional press release writing that follows Press Advantage guidelines and journalistic standards. Use when the user asks to write a press release, create a news announcement, draft a PR, or mentions Press Advantage distribution. Automatically generates LSI terms and industry entities, follows strict formatting rules (no lists/bullets/questions/headings in body, third-person only), and produces 600-750 word releases in objective journalistic style.
|
||||
---
|
||||
|
||||
# Press Release Writer
|
||||
|
||||
This skill creates professional press releases that comply with Press Advantage guidelines and standard journalistic conventions. The skill automatically handles LSI term generation, maintains proper structure, and ensures compliance with strict editorial requirements.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
When the user provides a press release topic, follow this workflow:
|
||||
|
||||
1. **Generate 7 Compliant Headlines**:
|
||||
- Immediately generate 7 unique, compliant headline options based on the topic
|
||||
- Each headline must be:
|
||||
- Maximum 70 characters
|
||||
- Title case
|
||||
- News-focused (not promotional)
|
||||
- Free of location keywords, superlatives (best/top/leading/#1), and questions
|
||||
- Contains actual news announcement
|
||||
- Present all 7 titles to an AI agent to judge which is best. This can be decided by looking at titles on Press Advantage for other businesses, and seeing how closely the headline follows the instructions.
|
||||
|
||||
2. **Gather Any Additional Required Information**:
|
||||
- If the user provides LSI terms explicitly, use them
|
||||
- If a URL is provided, fetch it for context
|
||||
|
||||
|
||||
3. **Automatic Generation**:
|
||||
- Generate LSI (Latent Semantic Indexing) terms relevant to the topic and industry
|
||||
- Identify relevant industry entities (companies, organizations, standards, technologies)
|
||||
- Research current industry context if needed
|
||||
- Lookup the company representative name and title from the md file based on the company name.
|
||||
|
||||
4. **Write the Press Release** following all requirements below
|
||||
|
||||
## Headline Generation Guidelines
|
||||
|
||||
When generating the 7 headline options:
|
||||
|
||||
**Variety in Approach**:
|
||||
- Mix different angles: announcement-focused, impact-focused, innovation-focused
|
||||
- Vary the structure while maintaining news format
|
||||
- Use different verbs: announces, launches, unveils, introduces, expands, achieves
|
||||
- Emphasize different aspects: product, partnership, milestone, expansion, award
|
||||
|
||||
**Character Count Management**:
|
||||
- Keep under 70 characters including spaces
|
||||
- Shorter is often better (55-65 characters is ideal)
|
||||
- Count carefully before presenting
|
||||
|
||||
**Compliance Checks**:
|
||||
- No questions (e.g., "Are You Ready for...?")
|
||||
- No location keywords (e.g., "Chicago," "Milwaukee," city or state names)
|
||||
- No superlatives (e.g., "Best," "Leading," "Top," "#1")
|
||||
- No promotional language (e.g., "Revolutionary," "Game-Changing")
|
||||
- Focus on the news, not the hype
|
||||
|
||||
**Examples of Good Headlines**:
|
||||
- "TechCorp Launches AI-Powered Customer Service Platform" (56 chars)
|
||||
- "Green Solutions Secures $50M Series B Funding Round" (52 chars)
|
||||
- "Acme Industries Expands Operations to European Markets" (55 chars)
|
||||
- "DataFlow Announces Strategic Partnership with IBM" (50 chars)
|
||||
- "HealthTech Achieves ISO 27001 Certification" (44 chars)
|
||||
- Also check the headlines.md file (if it exists) for other examples of good headlines.
|
||||
|
||||
**Examples of Bad Headlines** (DO NOT USE):
|
||||
- ❌ "Is Your Business Ready for AI Customer Service?" (question)
|
||||
- ❌ "Chicago's Leading TechCorp Launches New Platform" (location + superlative)
|
||||
- ❌ "Best-in-Class AI Solution Revolutionizes Support" (superlative + hype)
|
||||
- ❌ "TechCorp: The #1 Choice for Customer Service AI" (superlative + promotional)
|
||||
|
||||
## Critical Press Advantage Requirements
|
||||
|
||||
### Content Type
|
||||
- This is a PRESS RELEASE, not an advertorial, blog post, or promotional content
|
||||
- Must be objective news announcement written in journalistic style
|
||||
- Must announce actual NEWS (about products/services, milestones, awards, reactions to current events)
|
||||
- Must read like it could appear verbatim in a newspaper
|
||||
|
||||
### Writing Style - MANDATORY
|
||||
- **100% objective** - no hype, big claims, exclamation points, or sales messages
|
||||
- **Third-person ONLY** - except for direct quotes from executives
|
||||
- **NO first-person** ("I", "we", "our") except in quotes
|
||||
- **NO second-person** ("you", "your")
|
||||
- **NO questions** anywhere in headline or body
|
||||
- **NO lists, bullets, or numbered items** - write everything in paragraph form
|
||||
- **NO subheadings** in the body
|
||||
- **NO emoji**
|
||||
- **NO tables**
|
||||
- Perfect grammar and spelling required
|
||||
|
||||
### Word Count - CRITICAL
|
||||
- **MINIMUM: 575 words**
|
||||
- **TARGET: 600-750 words** (this is the sweet spot)
|
||||
- **MAXIMUM: 800 words**
|
||||
- Word count takes precedence over paragraph count
|
||||
- Typically 14-16 paragraphs for 600-750 word range
|
||||
|
||||
### Structure Requirements
|
||||
|
||||
**First Paragraph (Lead)**:
|
||||
- Must clearly identify the organization announcing the news
|
||||
- Must answer: Who, What, When, Where, Why
|
||||
- Should be 1-2 direct sentences
|
||||
- Contains the actual announcement
|
||||
|
||||
**Body Paragraphs**:
|
||||
- 2-4 sentences per paragraph maximum
|
||||
- Follow inverted pyramid structure (most important info first)
|
||||
- All quotes must be attributed to named individuals with titles
|
||||
- Use names and titles from any provided data files
|
||||
|
||||
**Headline**:
|
||||
- Selected from the 7 generated options (see Headline Generation Guidelines above)
|
||||
- Maximum 70 characters
|
||||
- Title case
|
||||
- One main keyword
|
||||
- NO location/geographic keywords (limits distribution)
|
||||
- NO superlatives (best, top, leading, #1)
|
||||
- NO questions
|
||||
- Must contain actual news announcement
|
||||
|
||||
### Call to Action
|
||||
- ✅ Acceptable: "Visit www.company.com to learn more" or "For more information, visit..."
|
||||
- ❌ Forbidden: "Buy now", "Sign up today", "Limited time offer", "Click here to purchase"
|
||||
|
||||
### What Gets REJECTED (Automatic Rejection)
|
||||
|
||||
**Advertorial Characteristics**:
|
||||
- Promotional tone ("revolutionary", "amazing", "best in class")
|
||||
- Product-centric messaging (features/benefits vs. news)
|
||||
- Customer testimonials or reviews
|
||||
- Sales-oriented calls to action
|
||||
- Opinion-based content
|
||||
- Personal perspectives
|
||||
|
||||
**Format Violations**:
|
||||
- Lists or bullets in body
|
||||
- Questions anywhere
|
||||
- Subheadings in body
|
||||
- First-person outside quotes
|
||||
- Excessive localization (city names in headlines)
|
||||
|
||||
## LSI Term Generation
|
||||
|
||||
When the user does NOT provide LSI terms explicitly, automatically generate them:
|
||||
|
||||
**What are LSI terms?**
|
||||
- Semantically related keywords and phrases
|
||||
- Industry-specific terminology
|
||||
- Related concepts and technologies
|
||||
- Synonyms and variations
|
||||
- Contextual language that signals topical relevance
|
||||
|
||||
**How to use LSI terms**:
|
||||
- Integrate naturally throughout the press release
|
||||
- Use 10-2 relevant LSI terms across the 600-750 words
|
||||
- Don't force keywords - maintain natural flow
|
||||
- Distribute terms across different paragraphs
|
||||
|
||||
**Example LSI terms for "sustainable packaging"**:
|
||||
- Biodegradable materials, circular economy, eco-friendly alternatives, carbon footprint reduction, recycled content, compostable solutions, environmental impact, waste reduction, green initiatives, packaging innovation
|
||||
|
||||
## Required Phrase Handling
|
||||
|
||||
If the user provides a specific phrase that must be included:
|
||||
- Use it exactly once in the body
|
||||
- Integrate it naturally into a relevant paragraph
|
||||
- Don't force it awkwardly
|
||||
|
||||
## URL Context Integration
|
||||
|
||||
If the user provides a URL:
|
||||
- Use web_fetch to retrieve the content
|
||||
- This will typically give you the factual background needed to write the release
|
||||
- Extract key facts, dates, names, and context
|
||||
- Use this information to enrich the press release
|
||||
- Maintain objectivity - don't copy promotional language
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finalizing, verify:
|
||||
- [ ] 600-750 words (minimum 550, maximum 800)
|
||||
- [ ] First paragraph clearly identifies the organization
|
||||
- [ ] Third-person throughout (except quotes)
|
||||
- [ ] No lists, bullets, questions, or subheadings in body
|
||||
- [ ] 2-4 sentences per paragraph
|
||||
- [ ] All quotes attributed with names and titles
|
||||
- [ ] LSI terms naturally integrated
|
||||
- [ ] Required phrase used exactly once (if provided)
|
||||
- [ ] Objective, journalistic tone
|
||||
- [ ] Perfect grammar and spelling
|
||||
- [ ] No promotional or sales language
|
||||
- [ ] Headline under 70 characters
|
||||
- [ ] Reads like it could appear in a newspaper
|
||||
|
||||
## Writing Approach
|
||||
|
||||
1. Start with the most newsworthy information in the lead
|
||||
2. Build credibility with specific details (dates, numbers, names)
|
||||
3. Include 1-2 executive quotes for human perspective
|
||||
4. Provide context about the company/organization
|
||||
5. Explain significance and impact
|
||||
6. End with company boilerplate and contact information
|
||||
7. Write in inverted pyramid style - can be cut from bottom up
|
||||
|
||||
## Tone Guidelines
|
||||
|
||||
- Professional and authoritative
|
||||
- Objective and factual
|
||||
- Confident but not boastful
|
||||
- Newsworthy, not promotional
|
||||
- Clear and concise
|
||||
- Industry-appropriate formality
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
- Using "we", "our", "you" outside of quotes
|
||||
- Including any lists or bullet points
|
||||
- Adding subheadings in the body
|
||||
- Writing in blog or editorial style
|
||||
- Making the headline a question
|
||||
- Focusing on product features instead of news
|
||||
- Including testimonials or reviews
|
||||
- Using promotional adjectives
|
||||
- Falling below 550 words or exceeding 800 words
|
||||
- Not clearly identifying the announcing organization
|
||||
- Forgetting to attribute quotes
|
||||
- Creating advertorial content instead of news
|
||||
Loading…
Reference in New Issue