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
PeninsulaInd 2026-02-15 18:04:29 -06:00
parent 1866d48cb2
commit b3140d3522
9 changed files with 1102 additions and 10 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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."

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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