AI Engineer
Deploy Streamlit Content Agent
0b29030
Raw
History Blame Contribute Delete
8.93 kB
"""Streamlit UI for the content generation agent.
Flow:
Sidebar: provider + API key + model + mock toggle (user pastes any key)
1. Connect website -> crawl + Brand Brain
2. Prompt helper -> suggestions + rough note -> editable brief
3. Generate -> ranked variants per channel with guardrail badges
4. PPT export -> download .pptx
"""
import os
import tempfile
import streamlit as st
from core.config import (get_settings, MODEL_CHOICES, DEFAULT_MODELS)
from core.pipeline import ContentAgent
from core.schemas import Brief
st.set_page_config(page_title="Content Agent", page_icon="✍️", layout="wide")
def get_agent() -> ContentAgent:
s = st.session_state
settings = get_settings(
provider=s.get("provider"),
api_key=s.get("api_key") or None,
model=s.get("model") or None,
mock_mode=s.get("mock_mode", True),
)
return ContentAgent(settings)
# ---------------- Sidebar: model / key config ----------------
with st.sidebar:
st.header("⚙️ Model settings")
st.caption("Paste your own API key and pick a model, or run in mock mode.")
st.session_state["mock_mode"] = st.toggle(
"Mock mode (no key needed)", value=st.session_state.get("mock_mode", True))
provider = st.selectbox("Provider", ["groq", "openrouter"],
index=0, disabled=st.session_state["mock_mode"])
st.session_state["provider"] = provider
st.session_state["api_key"] = st.text_input(
"API key", type="password", disabled=st.session_state["mock_mode"],
placeholder="Paste Groq or OpenRouter key")
model_options = MODEL_CHOICES.get(provider, [])
chosen = st.selectbox("Model", model_options,
index=0, disabled=st.session_state["mock_mode"])
custom = st.text_input("Custom model (optional)",
disabled=st.session_state["mock_mode"],
placeholder="override model id")
st.session_state["model"] = custom or chosen
if st.session_state["mock_mode"]:
st.info("Running in mock mode — deterministic sample output, $0 cost.")
elif not st.session_state["api_key"]:
st.warning("Enter an API key or enable mock mode.")
st.title("✍️ Content Generation Agent")
st.caption("Daily product & marketing content for founders — grounded, guardrailed, multi-channel.")
# ---------------- Step 1: website ----------------
st.subheader("1️⃣ Connect your website")
col1, col2 = st.columns([3, 1])
with col1:
website = st.text_input("Product website URL", placeholder="https://yourproduct.com")
with col2:
st.write("")
st.write("")
crawl_btn = st.button("Crawl & analyze", type="primary", use_container_width=True)
if crawl_btn and website:
with st.spinner("Crawling website and building Brand Brain…"):
try:
agent = get_agent()
brand = agent.ingest_website(website)
st.session_state["brand"] = brand.to_dict()
st.success(f"Brand Brain ready for {brand.product_name}")
except Exception as e:
st.error(f"Failed: {e}")
if st.session_state.get("brand"):
b = st.session_state["brand"]
with st.expander("🧠 Brand Brain (review the grounded facts)", expanded=False):
st.write(f"**{b['product_name']}** — {b['one_liner']}")
c1, c2 = st.columns(2)
c1.write("**Value props**")
c1.markdown("\n".join(f"- {x}" for x in b.get("value_props", [])) if b.get("value_props") else "_None_")
c1.write("**Features**")
c1.markdown("\n".join(f"- {x}" for x in b.get("features", [])) if b.get("features") else "_None_")
c2.write("**Differentiators**")
c2.markdown("\n".join(f"- {x}" for x in b.get("differentiators", [])) if b.get("differentiators") else "_None_")
c2.write("**Cannot claim (guardrail)**")
c2.markdown("\n".join(f"- {x}" for x in b.get("forbidden_claims", [])) if b.get("forbidden_claims") else "_None_")
# ---------------- Step 2: prompt helper + brief ----------------
if st.session_state.get("brand"):
from core.schemas import BrandBrain
brand_obj = BrandBrain(**st.session_state["brand"])
st.subheader("2️⃣ Describe today's update")
if st.button("💡 Suggest prompts"):
with st.spinner("Thinking of prompt ideas…"):
st.session_state["suggestions"] = get_agent().suggest_prompts(brand_obj)
for sug in st.session_state.get("suggestions", []):
st.caption("→ " + sug)
raw = st.text_area("Your rough note", placeholder="e.g. shipped one-click integrations today")
cc1, cc2, cc3 = st.columns(3)
channels = cc1.multiselect("Channels", ["linkedin", "instagram", "whatsapp"],
default=["linkedin", "instagram", "whatsapp"])
num_variants = cc2.slider("Variants per channel", 1, 5, 3)
language = cc3.selectbox("Language", ["en", "hi (coming soon)"], index=0)
lang_code = "en" if language.startswith("en") else "hi"
if st.button("Build editable brief"):
if not channels:
st.error("Please select at least one channel in Step 2.")
else:
with st.spinner("Structuring your brief…"):
built = get_agent().build_brief(raw, brand_obj, channels, num_variants, lang_code)
st.session_state["brief"] = built["brief"].to_dict()
st.session_state["suggested_prompt"] = built["suggested_prompt"]
if st.session_state.get("brief"):
st.subheader("3️⃣ Review & customize the brief")
br = st.session_state["brief"]
pcol1, pcol2 = st.columns(2)
br["objective"] = pcol1.selectbox(
"Objective", ["daily_update", "feature", "launch", "learning", "hiring", "metric"],
index=max(0, ["daily_update", "feature", "launch", "learning", "hiring", "metric"].index(br["objective"]) if br["objective"] in ["daily_update", "feature", "launch", "learning", "hiring", "metric"] else 0))
br["tone"] = pcol2.selectbox(
"Tone", ["professional", "founder_story", "casual", "punchy"],
index=max(0, ["professional", "founder_story", "casual", "punchy"].index(br["tone"]) if br["tone"] in ["professional", "founder_story", "casual", "punchy"] else 0))
br["audience"] = pcol1.text_input("Audience", value=br.get("audience", ""))
br["cta"] = pcol2.text_input("CTA", value=br.get("cta", ""))
st.session_state["suggested_prompt"] = st.text_area(
"Suggested prompt (edit freely)", value=st.session_state.get("suggested_prompt", ""))
br["raw_input"] = st.session_state["suggested_prompt"]
st.session_state["brief"] = br
if st.button("🚀 Generate content", type="primary"):
if not br.get("channels"):
st.error("Please select at least one channel in Step 2 before generating content.")
else:
with st.spinner("Generating grounded, guardrailed content…"):
agent = get_agent()
brand_obj = BrandBrain(**st.session_state["brand"])
brief_obj = Brief(**br)
result = agent.generate(brief_obj, brand_obj)
st.session_state["result"] = result
# ---------------- Step 4: results + PPT ----------------
if st.session_state.get("result"):
result = st.session_state["result"]
st.subheader("4️⃣ Generated content")
if not result.variants_by_channel:
st.warning("⚠️ No channels were selected or generated.")
else:
tabs = st.tabs([c.capitalize() for c in result.variants_by_channel.keys()])
for tab, (channel, variants) in zip(tabs, result.variants_by_channel.items()):
with tab:
for i, v in enumerate(variants):
badge = "✅ clean" if v.guardrails.passed else "⚠️ " + str(len(v.guardrails.all_flags())) + " flag(s)"
st.markdown(f"**Variant {i+1}** · score {v.score} · {badge}")
st.text_area(f"{channel}_{i}", value=v.text, height=140, label_visibility="collapsed")
if v.hashtags:
st.caption(" ".join(v.hashtags))
if not v.guardrails.passed:
for f in v.guardrails.all_flags():
st.caption("⚠️ " + f)
st.divider()
st.subheader("📊 Export PPT")
if st.button("Generate PPT deck"):
with st.spinner("Building deck…"):
out = os.path.join(tempfile.gettempdir(), "content_update.pptx")
path = get_agent().export_ppt(result.fact_pack, result.brief, out)
with open(path, "rb") as fh:
st.download_button("⬇️ Download .pptx", fh.read(),
file_name="content_update.pptx",
mime="application/vnd.openxmlformats-officedocument.presentationml.presentation")