Don't search for a movie. Describe the story you want to experience.
An agentic narrative-discovery system built on Strands Agents + Amazon
Bedrock. Converts a free-form story description into a structured
Narrative Fingerprint, searches a curated movie corpus, verifies
evidence, ranks candidates, explains matches/mismatches, and refines on
feedback. Built for the "Agents for Humans" (Strands Agents) hackathon --
see ../StoryMatch_Agent_Research.md on the Desktop for the full product
research doc this scaffold implements.
Timeline so far (account 034456343698, us-east-1):
- Bedrock initially denied every model (Claude, Nova) with
INVALID_PAYMENT_INSTRUMENT-- no valid card on the account. Fixed by adding a payment method in Billing -> Payment preferences. - After that, Bedrock switched to
ThrottlingException: Too many tokens per day. Root cause (confirmed via Service Quotas API): adding/changing a payment method triggers a standard AWS account verification step (aus-east-2call surfaced the explicit message: "Your account is currently being verified. Verification normally takes less than 2 hours."). During verification, Bedrock's daily token quota for every model sits at a hard 0 and cannot be raised manually (Service Quotasconfirms these particular quotas are not adjustable) -- it lifts automatically once verification finishes. - Nothing to do but wait it out (typically <2h, occasionally longer for brand-new accounts). Re-check with:
source .venv/bin/activate
python -c "
import boto3
c = boto3.client('bedrock-runtime', region_name='us-east-1')
r = c.converse(modelId='us.anthropic.claude-sonnet-4-5-20250929-v1:0',
messages=[{'role':'user','content':[{'text':'Reply with exactly: OK'}]}])
print(r['output']['message']['content'])
"This did not block the build. The whole agent is verified working
end-to-end right now via STORYMATCH_MODEL_PROVIDER=anthropic (direct
Anthropic API) -- ran the full two-turn scripted demo successfully,
including the agent adaptively relaxing search constraints across multiple
search_movies calls when the first attempt came back empty, and honestly
reporting when the 82-movie corpus has no great match instead of
overselling a mediocre one. Switch back to bedrock (or unset the env var)
once verification clears -- that's what the hackathon rubric rewards.
data/movies.json-- 82 hand-curated films (including superhero, rom-com, and straight-comedy titles specifically soavoid_elementsexclusions have something real to filter out), each pre-tagged with a derived narrative representation: setting, threat, protagonist, central_conflict, relationships, themes, tone, pacing, action_intensity, ending type. This is the "Derived Narrative Representation" layer from the research doc (section 16) -- the actual product differentiation.storymatch/dataset.py-- the retrieve -> hard-filter (avoid exclusions) -> verify (per-dimension evidence) -> rank -> diversify pipeline (sections 17-19 of the doc), pure Python, no vector DB needed for 40 movies.storymatch/memory.py-- local JSON taste memory (likes/dislikes per user). Upgrade path to Bedrock AgentCore Memory documented inline.storymatch/tools.py-- 4 Strands@toolfunctions:search_movies,get_movie_details,get_taste_memory,update_taste_memory.storymatch/agent.py-- the StrandsAgent, wired to Bedrock (or Anthropic as a fallback), with a system prompt that encodes the full workflow: fingerprint extraction, contradiction detection, ambiguity clarification, evidence-grounded explanation, feedback-driven refinement, and quiet taste-memory updates.app.py-- Streamlit chat UI. The agent's own markdown response (match %, ASCII evidence bars, why-it-matches/why-it-may-not) is the UI; Streamlit just renders it and shows the tool-call trace so judges can see the agent actually doing multi-step work.cli_demo.py-- terminal fallback, including a--scriptedmode that replays the doc's example conversation unattended (recorded backup in case venue wifi or Bedrock hiccups during the live demo).tests/-- pytest suite for everything that doesn't need a live model (schema integrity, exclusion logic, ranking, diversity, hidden gem mode, agent wiring). Run withpython -m pytest tests/ -q.
These aren't just in the system prompt -- each was run against a live model
(Anthropic API, while Bedrock was blocked) and behaved correctly with no
extra tool code, because Claude's own reasoning plus the workflow
instructions in agent.py's SYSTEM_PROMPT cover them:
- Contradiction detection (doc's killer feature #6) -- asked for "a slow, quiet psychological drama, but also nonstop explosive action the whole time" and the agent stopped and asked which should take priority before searching, instead of averaging the two into a mediocre query.
- Counterfactual search (doc's killer feature #8) -- after getting zombie-survival results, asked "what if I remove the zombie requirement entirely?" and the agent re-ran the search with only that constraint dropped, and explicitly called out which prior picks got weaker without it (e.g. "Train to Busan -- zombies, you just dropped that").
- Adaptive retrieval -- when the first
search_moviescall for a narrow request came back with poor matches, the agent relaxed constraints (ending type, then group size) across several moresearch_moviescalls on its own before presenting results, rather than forcing a bad top-5. This is the strongest "this is actually an agent, not a wrapper" evidence -- see the case-file artifact's Exhibit C/D. - Story mutation -- asked to keep the same setup "but make it sci-fi
instead of zombies," the agent separated narrative mechanics (small
group, psychological tension, turning on each other, slow pacing --
kept) from surface dressing (zombies/post-apocalyptic -- dropped),
said so explicitly (
DROPPED: zombies... KEPT: small group...), and re-ranked with a genuine sci-fi title (Cube) on top instead of a zombie film with the label changed.
Built with actual code (not just prompt behavior): Hidden Gem Mode --
search_movies(..., prioritize_hidden_gems=True) nudges ranking toward
low-popularity titles with comparable narrative fit, mirroring the doc's
ranking formula giving popularity a deliberately small weight. Verified in
tests/test_dataset.py.
Cut for time -- add only if the MVP demo is solid and there are hours left:
- Bedrock AgentCore Memory --
storymatch/agentcore_memory.pyhas a documented sketch (setup steps, what to implement) but it'sNotImplementedErrorby design: it was written while Bedrock access was blocked, so it's unverified and NOT wired intotools.py. The proven local-JSONstorymatch/memory.pyis what the demo actually runs on. Only flip the switch (see that file's docstring) after testing the AgentCore path standalone -- a silently-broken memory backend is worse than the file it replaced. - AgentCore Runtime deployment -- running locally only. Doc section 22 has the target architecture if there's time to deploy.
- The 50-100 prompt evaluation benchmark (doc section 30).
cd StoryMatch-Agent
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # edit if neededAWS credentials are read from ~/.aws/credentials (default profile is
already configured, region us-east-1) -- nothing else to set up for
Bedrock once the payment-instrument issue above is fixed.
source .venv/bin/activate
# Web demo
streamlit run app.py
# Terminal demo
python cli_demo.py # interactive
python cli_demo.py --scripted # unattended, matches the doc's demo script
# Local dev without waiting on AWS billing:
STORYMATCH_MODEL_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-... streamlit run app.pyReset taste memory between demo runs: rm data/taste_memory.json.
I want a dark movie about survivors after a zombie apocalypse. Small
group, lots of betrayal and distrust, more psychological than action,
and I don't want a happy ending.
Then: "I liked #2. Give me something even darker, but not horror."
This exercises the full loop: fingerprint extraction -> search_movies ->
ranked results with evidence -> feedback -> refined search_movies call ->
update_taste_memory. That two-turn loop is the core "agency" proof point
for judges -- see doc section 21, "Why This Is Actually an Agent."
User story (free text)
|
v
Strands Agent (Bedrock Claude) <-- system_prompt encodes the whole
| workflow: fingerprint, contradiction/
| tool calls ambiguity checks, explanation style,
v feedback handling, memory hygiene
get_taste_memory
search_movies --> storymatch/dataset.py: retrieve -> hard filter
(avoid_elements) -> per-dimension verify -> weighted
rank -> greedy diversity pass
get_movie_details
update_taste_memory --> storymatch/memory.py: local JSON, per user_id
|
v
Markdown response: match %, ASCII evidence bars, why/why-not, ask for
feedback
82 movies is enough for a convincing demo across the query types in the
doc (post-apocalyptic survival, psychological thrillers, simulated-reality
sci-fi, tragic romance, folk horror, dystopian survival games, war drama,
heist/crime, superhero/comedy for exclusion testing). To grow it further:
add entries to data/movies.json following the existing schema, or write a
data/prep_dataset.py that pulls from TMDb (metadata + overview) and asks
a Bedrock model to derive the narrative tags in batch -- the doc's
recommended data strategy (section 16). Not built here to avoid needing a
TMDb API key as a hard dependency for the MVP demo.
People don't think in genres. They think in stories. StoryMatch is an agent that turns an imagined story into a searchable narrative fingerprint, investigates a movie corpus, verifies the story fit, and learns what kinds of stories you actually like.