Ctrl K
ComunidadGuías…

config.yaml.example

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Morning Signal — Configuration
# Copy to config.yaml and edit for your environment.

# ── S3 / Hosting ────────────────────────────────────────────────────────────
# The S3 bucket where episodes + feed are published.
# Set up with public read access or CloudFront (see README).
s3_bucket: "your-bucket-name"
s3_region: "us-west-2"
s3_prefix: ""  # optional subfolder, e.g. "podcast/"
# Cloud-deploy only: S3 prefix the prompt files are loaded from when running
# with MORNING_SIGNAL_USE_SSM=1. Local runs read prompt.md from disk and ignore this.
prompts_s3_prefix: "prompts/"

# The public base URL where your files are served.
# If using CloudFront: https://d1234abcd.cloudfront.net
# If using S3 static hosting: https://your-bucket-name.s3.us-west-2.amazonaws.com
base_url: "https://your-bucket-name.s3.us-west-2.amazonaws.com"

# ── Podcast Metadata (Apple Podcasts / RSS) ─────────────────────────────────
podcast:
  title: "Morning Signal"
  description: "A sharp daily briefing on markets, tech, AI, and Seattle — auto-generated."
  author: ""
  email: ""  # optional, for iTunes owner tag
  language: "en-us"
  category: "Business"
  subcategory: "Investing"
  explicit: false
  artwork: "artwork.jpg"  # 3000x3000 recommended. Place in project root.

# ── TTS ─────────────────────────────────────────────────────────────────────
tts:
  engine: "polly"          # "polly" (Amazon, uses AWS creds) or "google" (Chirp3 HD, uses GOOGLE_APPLICATION_CREDENTIALS)
  # Polly (engine: polly)
  polly_voice: "Ruth"      # Ruth = British female, Matthew = US male, Joanna = US female
  polly_engine: "neural"   # "neural" (best quality) or "standard"
  # Google Chirp3 HD (engine: google) — pip install '.[google]'; all Chirp3 voices same price (1M chars/mo free)
  google_voice: "en-US-Chirp3-HD-Leda"
  google_language: "en-US"
  google_max_sentence_chars: 400   # Chirp3 HD rejects any single sentence over its internal cap; we pre-split run-ons longer than this at clause boundaries (keep < ~500)
  speed: 1.0               # Playback speed multiplier; 1.0 = normal conversational pace, raise to 1.25–1.5 for fast brief

# ── Schedule ────────────────────────────────────────────────────────────────
# Dates to skip ENTIRELY (no episode, either edition) — travel days, vacations.
# ISO YYYY-MM-DD strings; malformed entries fail loud at startup. Distinct from
# the NYSE trading calendar (weekends/holidays still ship a weekend AM): a skip
# date produces nothing, and the freshness watchdog treats the absent episode
# as expected instead of alerting. `generate --force` overrides for a one-off.
# skip_dates:
#   - "2026-07-09"
#   - "2026-07-10"
skip_dates: []

# Scheduled per-date content overrides (OPTIONAL ADD-ON, default OFF).
# When enabled, a small schedule manifest JSON is read from S3 at generation
# time; an entry for today's date can (a) "override" — devote the whole
# episode to a scheduled deep-dive topic instead of regular programming,
# (b) "extend" — add one extra segment on top of the regular lineup, or
# (c) "skip" — produce no episode at all that day (the console-editable
# counterpart of the skip_dates list above; both sources are honored, and
# `generate --force` overrides either). Basic form is unaffected: with the
# block absent or enabled=false there is ZERO behavior change — no S3 read,
# no new dependencies, no required objects. Even when enabled, a missing
# manifest is a silent no-op; any schedule READ failure fails soft (the
# regular episode ships) with a WARN log + Telegram alert. Manifest schema:
# docs/schedule-schema.json (schema_version 1).
# schedule:
#   enabled: true
#   s3_bucket: ""                      # default: s3_bucket above
#   s3_key: "schedule/schedule.json"
schedule:
  enabled: false

# ── Generation ──────────────────────────────────────────────────────────────
# Active model spec via the krepis provider-agnostic adapter. Format:
# "provider:model" (or a krepis ModelSpec JSON object as a string).
# When omitted, falls back to the legacy anthropic + claude_model pair below —
# behavior-identical for pre-migration configs.
#
# Grounding depends on the transport:
#   - anthropic / openrouter — server-side web_search (native tool or
#     openrouter:web_search). Coverage guards + forced-search recovery apply
#     (force is anthropic-only; openrouter degrades to prose forcing).
#   - litellm (krepis router model groups: low/med/high/ultra) — no server-
#     side search. Grounding comes from the pre-fetched news_context digest
#     (enable news_context: below). The citation / required_search_topics
#     guards are skipped on this path rather than falsely failing.
#
# Production (dashboard box) uses the krepis ``high`` group via the local
# LiteLLM proxy — litellm falls through the group's own chain
# (deepseek-v4-pro-max → grok-4 → qwen3-max → openrouter deepseek-v4-pro).
# Env override: MORNING_SIGNAL_LLM.
# llm: '{"provider": "litellm", "model": "high"}'
# llm: "anthropic:claude-sonnet-4-6"
claude_model: "claude-sonnet-4-6"  # legacy knob — feeds the default spec

# Output-token budget. On the direct-anthropic path (no `llm:` router group
# set above) this is the only budget there is, and 8192 is a sane default.
#
# On the krepis router path (`llm: '{"provider": "litellm", ...}'`), leave
# this UNSET — omitting it lets krepis use the registry's own budget for
# whatever model the group resolves to, which model-router-policy §2 makes
# a registry-owned parameter. Setting it here OVERRIDES that row on every
# call, including for a reasoning model, where max_tokens bounds reasoning
# AND content from one shared pool — a budget sized for a non-reasoning
# answer can starve content generation entirely before it starts
# (alpha-engine-config-I6901). Uncomment only if you have measured that a
# specific model's registry row is wrong for your workload.
# max_tokens: 8192

# Hard cap on Anthropic ``web_search`` server-tool requests per episode.
# Insurance against runaway loops — Anthropic bills $10 per 1,000 search
# requests. Typical episodes use ~15 searches; 20 sits above that as a
# ceiling, not a throttle. Omit to use the in-code default of 20.
web_search_max_uses: 20

# Minimum script length, in spoken words, below which generation ABORTS the
# attempt (alpha-engine-config-I11831). A near-empty or truncated script that
# passes every other guard used to go straight to TTS/publish and was announced
# with the same check mark as a full episode (2026-09-21: ~200 words aired as a
# 53-second episode). Below the floor the attempt is treated like the tool-call
# and grounding guards: the fallback tier runs instead of the short script
# airing, and a short episode that somehow publishes is flagged in the success
# ping. Default 500 (~2:15 spoken): 2.5x the 09-21 outlier, 0.66x the lowest
# normal edition in the 30 days measured (~758 words). Either an int for every
# edition, or a mapping keyed by edition (am / pm / weekend) with an optional
# ``default``; 0 disables the floor for that edition. A malformed value falls
# back to the default rather than disabling the guard.
# min_script_words: 500
# min_script_words: {am: 500, pm: 500, weekend: 500}

# Minimum web searches an edition must perform or generation ABORTS before
# publish (fail-loud). A zero/low-search edition is almost always
# model-confabulated rather than grounded in live news — this guard stops a
# hallucinated episode from going live silently (2026-06-16 incident: a
# pre-fetched news block told the model it could skip search, so it ran 0
# searches and fabricated the whole episode). Default 1. Set to 0 only if
# your prompt legitimately needs no live web search.
min_web_searches: 1

# Per-segment search guard (optional, default none). ``min_web_searches`` above
# only asserts the edition was grounded *somewhere* — it does NOT guarantee a
# *specific* search-critical segment was covered. The failure mode this catches
# (2026-06-17): with a tight ``web_search_max_uses`` budget the model spends its
# searches on the earlier segments and reaches a no-digest segment (one with no
# pre-fetched fallback) with no budget left, then writes it from memory. Declare
# a topic per such segment, listing keywords any of which a search query must
# contain; if fewer than ``min_matches`` (default 1) searches hit a topic, the
# topic is "uncovered". Default: none (no-op).
#
# When a topic is uncovered the DEFAULT is publish-anyway + alert: the episode
# still ships (every other segment is grounded), and a flow-doctor/Telegram
# notification names the uncovered topics for async triage. Set
# ``required_search_topics_fatal: true`` to restore the legacy behaviour —
# ABORT before publish — for deployments that would rather skip an edition than
# ship one stale segment. (The global ``min_web_searches`` floor always hard-
# aborts regardless: a near-zero-search edition is hallucinated wholesale.)
# required_search_topics_fatal: false
#
# SELF-HEALING (default on): before the publish-or-abort decision, an uncovered
# topic triggers ONE bounded recovery regeneration whose user message names the
# dropped segment(s) + their keyword figures, FORCES a dedicated web search via
# tool_choice (not just an in-prose ask), and requires a written segment for
# each. The recovered draft is adopted only if it covers
# strictly more; otherwise the original draft falls through to the policy above.
# Recovery runs even in fatal mode (so a hard-abort fires only when the forced
# retry still can't cover). Coverage is judged with the SCRIPT in hand — a topic
# counts as covered only when it was both searched AND its segment actually
# aired, so an unrelated segment's search can't falsely satisfy it. Set to false
# to skip recovery and go straight to publish+alert / abort.
# required_search_topics_recover: true
#
# ``editions`` (optional) scopes a topic to specific editions — useful when a
# weekday edition has a segment (e.g. a political pulse) that the "weekend"
# edition's prompt does not. Edition values are "am", "pm", and "weekend"
# (non-trading days). A topic with an ``editions`` list is enforced ONLY for
# those editions, so a weekday-only topic never falsely aborts the weekend pod.
# Omit ``editions`` to enforce on every edition. Example for a political-pulse
# segment sourced only from Truth Social / X, weekday editions only:
#
# required_search_topics:
#   - name: "Political pulse"
#     keywords: ["truth social", "trump", "maga"]
#     min_matches: 2
#     editions: ["am", "pm"]    # not "weekend" — the weekend prompt has no politics
#   - name: "Seattle local"
#     keywords: ["seattle", "cascades"]
#     min_matches: 1

# ── Pre-fetched News Context (optional, default OFF) ────────────────────────
# When enabled, morning-signal reads a pre-built news digest JSON from S3 and
# injects it into the Claude prompt as a SUPPLEMENTARY starting reference —
# leads for the model to verify. It does NOT suppress web search: the model is
# still instructed to web-search every segment (and the min_web_searches guard
# above enforces that it does). Leave disabled (the default) to keep the
# standard web-search-only behavior — when off, this block has zero effect.
#
# The digest is a JSON object of the shape
#   {"schema_version": 1, "date": "...", "sections": {"<name>": [<item>, ...]}}
# where each item has a `title` (required) plus optional `ticker`, `source`,
# `published`, `excerpt`, `url`.
#
# `required` (default true when enabled) makes the digest a HARD prerequisite:
# if it is missing, malformed, STALE (its `date` != the episode's date), or
# empty, generation ABORTS before publish — no valid fresh digest, no pod. This
# stops a soft-failed digest from silently degrading into a pod narrated without
# the news it was meant to carry. Set `required: false` to fall back to the
# fail-soft behavior (warn + generate with web search only).
news_context:
  enabled: false
  required: true                                  # missing/stale/empty digest → abort the pod (set false for fail-soft)
  s3_bucket: ""                                   # bucket holding the digest JSON (required when enabled)
  s3_key: "data/news_digest_daily/latest.json"    # object key for the digest (default shown)

# The episode prompt is fully yours to customize: edit ``prompt.md`` (and
# ``prompt_weekend.md`` for weekend/holiday editions). See the ``data/prompt-*``
# starters (markets-only, tech-only, generic-news, local-news, blank) for
# ready-made variants to copy from.

# Max episodes to keep in the RSS feed (older episodes remain in S3 but
# drop off the feed to keep it lightweight).
feed_max_episodes: 90

# ── Notifications (optional, via Telegram / flow-doctor) ───────────────────
# When enabled, sends a one-line success ping after each successful run
# and a failure ping with traceback on any uncaught exception, both
# routed via flow-doctor's TelegramNotifier to a single bot + chat
# (optional forum topic).
#
# Setup (two minutes):
#   1. Message @BotFather → /newbot → save the bot token.
#   2. Add the bot to your chat (or create a fresh group / supergroup).
#   3. Send the bot a message, then look up the chat_id at
#      https://api.telegram.org/bot<TOKEN>/getUpdates and read
#      result[0].message.chat.id (negative integer for groups /
#      supergroups / channels).
#   4. Set FLOW_DOCTOR_TELEGRAM_BOT_TOKEN + FLOW_DOCTOR_TELEGRAM_CHAT_ID
#      in the env, OR inline the values under telegram_* below.
#
# Forum supergroups: optional `telegram_message_thread_id` routes
# notifications to a specific topic in the group — handy for fanning
# am / pm / other-flow alerts through one chat without N bots.
notifications:
  enabled: false
  telegram_bot_token: ""              # or set FLOW_DOCTOR_TELEGRAM_BOT_TOKEN in env
  telegram_chat_id: ""                # or set FLOW_DOCTOR_TELEGRAM_CHAT_ID in env
  telegram_message_thread_id: null    # optional: forum-topic routing