FrameIQ:基于 LangGraph 多智能体 AI 助手的影视社交平台
RobinMillford/FrameIQ
FrameIQ 是一个 Letterboxd 风格的影视社交平台,内置基于 LangGraph 的多智能体 AI 助手 CineBot,可自然语言推荐、实时流式输出推理过程。
A cinematic social movie & TV platform with a multi-agent AI assistant.
Track, discover, and discuss what you watch — powered by LangGraph, Flask, and TMDb.
🎬 What is FrameIQ?
FrameIQ is a Letterboxd-inspired social platform for film and TV enthusiasts, elevated with a production-grade multi-agent AI assistant (CineBot) that understands natural language, recommends by vibe, and streams its reasoning in real-time.
Built for people who care about what they watch — and want an intelligent companion to help them discover more.
| Track everything you watch | Discover by mood, genre, era | Discuss with friends & community |
| Watch inline with resume | Analyze your taste DNA | Chat with an AI film expert |
✨ Features
📚 Library & Tracking
- Watchlist with priority tiers (High / Medium / Low)
- Diary — chronological log with dates, ratings, notes
- Viewed library with rich filtering
- Star ratings (½–5★) + written reviews with markdown
- Custom lists — public, private, collaborative
- Tags with autocomplete & trending suggestions
📺 TV Show Tracking (First-Class)
- Episode-by-episode progress with timestamps
- Batch season operations — mark entire seasons watched
- Smart completion — only marks Completed when TMDb confirms series ended
- Upcoming Episodes calendar — 60-day view, auto-synced daily
- Continue Watching row with resume points
👥 Social Layer
- Follow / unfollow with activity feeds (Following / Global / Personal)
- Friends' activity on every title page
- Popular With Friends algorithmic ranking
- Review likes, comments, helpful votes
- Suggested follows based on taste overlap
▶️ Inline Watching
- Stream movies & episodes directly — no redirects
- Resume playback with per-title progress persistence
- Auto-logs diary entry at 85% completion
- Full watch history with timestamps
🔍 Discovery & Search
- TMDb-powered filters: genre, year, language, rating, provider
- Trending movies & shows (daily/weekly)
- Entertainment news feed (NewsAPI)
- Semantic similarity — "movies that feel like a rainy Sunday"
🤖 CineBot — Multi-Agent AI Assistant
Architecture: LangGraph pipeline
User message
│
▼
┌─────────────┐
│ Supervisor │ ← zero-LLM heuristic router (saves 2–3 API calls)
└──────┬──────┘
│
┌───┴───┐
▼ ▼
┌───────┐ ┌─────┐
│Retriever│ │ Chat│ ← Retriever: 7 TMDb/vector tools
│ │ │ │ Chat: open-ended film knowledge
└───┬───┘ └──┬──┘
└────┬────┘
▼
┌──────────┐
│ Enricher │ ← concurrent TMDb poster & metadata fetch
└────┬─────┘
▼
SSE stream → browser (markdown + poster cards)
| Agent | Role | Model |
|---|---|---|
| Supervisor | Route + extract entities (titles, people, genres, years) | gpt-4.1-mini (structured) |
| Retriever | ReAct agent with 7 cached tools | gpt-4.1-mini |
| Chat | General film knowledge, recommendations | gpt-5-mini |
| Enricher | Extract titles from reply → fetch posters/meta | gpt-4.1-mini |
- Streaming responses via SSE — see each tool call as it happens
- Poster & metadata cards injected inline in replies
- Conversation memory persisted (SQLite checkpointing, survives restarts)
- User personalization — ratings, favorite genres, TV progress, watchlist injected per request
- Command palette (⌘K) + Slide-over panel (⌘J) for keyboard-first UX
- Production-hardened streaming: single process event loop owns the
AsyncSqliteSavercheckpointer (no cross-loop use); graph + checkpoint verified before headers commit or the API returns503 JSON(mid-stream failures arrive as SSEerrorevents, never a status flip); per-stream checkpoint threads so concurrent chats can't collide - Limits: 5 questions per user per UTC day, 20/min + 100/hour per IP —
the UI shows remaining quota live via
X-Chat-Remainingheaders
📊 Personal Analytics
- Watch time, genre breakdown, top directors/actors
- Year-in-review with interactive Chart.js visualizations
- Taste DNA — genre affinity bars with gradient fills
- Taste Match badges on every title page
🏗 Tech Stack
| Layer | Technology |
|---|---|
| Backend | Flask 3.1, SQLAlchemy 2.0, PostgreSQL 16 |
| AI / Agents | LangGraph, LangChain, OpenAI (gpt-4.1-mini / gpt-5-mini) |
| Auth | Google OAuth 2.0 (Authlib) + Flask-Login + Flask-WTF CSRF |
| Frontend | Jinja2, Tailwind CSS, Vanilla JS (ESM), Chart.js, Lucide icons |
| Media APIs | TMDb, Cloudinary (avatars), NewsAPI |
| Streaming | Rive, VidKing, Vidy, 1Embed providers |
| Infra | Docker Compose, systemd-nginx (reverse proxy), GitHub Actions |
| Observability | Langfuse-ready, structured logging |
🎨 Design System
Monochrome Marquee — true black (#000), white type, single amber accent (#F6B73C)
- Display: Archivo Expanded (variable width)
- Body: Inter
- Metadata: JetBrains Mono
- Motion: cubic-bezier(0.16, 1, 0.3, 1) — 180ms/320ms
- Radii: 4 / 6 / 10px — sharp, broadcast-grade
- Film grain overlay + projector beam/dust atmosphere
All 47 templates unified on a single design token system. Zero Poppins, zero indigo/violet, zero unused dependencies.
🚀 Quick Start
Prerequisites
- Python 3.12+
- PostgreSQL 16 (local or Docker)
- API keys (see Environment Variables)
Local Development
# 1. Clone & enter git clone https://github.com/RobinMillford/FrameIQ.git cd FrameIQ # 2. Virtual environment python -m venv .venv && source .venv/bin/activate # 3. Dependencies (uv recommended) uv sync # or: pip install -r requirements.txt # 4. Environment cp .env.example .env # Edit .env with your keys # 5. Run python app.py # http://localhost:5000
Run Tests
pytest tests/ -v # offline suite (~7 min), no TMDb key needed uv run flake8 . # lint (max-line 127, complexity 10)
Docker (Production Stack)
cp .env.example .env # production values make deploy # pull + clean build + start (run from /opt/frameiq on the VPS)
Stack: web (Gunicorn 2 workers × 4 threads, Python 3.11-slim) + db (Postgres 16-alpine). TLS terminates at the host nginx (ports 80/443, certbot); Postgres is bound to loopback only — never exposed to the internet.
make status # container status + /agent_health check make logs # tail web logs (logs-db for Postgres) make restart # restart web container make backup # pg_dump snapshot via scripts/backup.sh make db-shell # psql into the production DB make migrate # status of the versioned migration ledger (READ-ONLY)
Schema changes (new tables AND new columns)
The startup schema guard REQUIRES schema parity with the declared models and will fail Gunicorn workers if the database is behind — that guard is intentional and must not be weakened.
Application startup never creates schema. There is no db.create_all() at
runtime: not for new tables, not for new columns. Every schema change — new
table or new column — is applied by the versioned migration runner, before
the new web image starts:
git pull origin main
docker compose build web
docker compose run --rm --no-deps web \
python scripts/migrate.py init-ledger
docker compose run --rm --no-deps web \
python migrates/migrate_schema_convergence.py
docker compose run --rm --no-deps web \
python scripts/migrate.py upgrade --allow-production
docker compose up -d
docker compose exec web python -m utils.schema_guardRun the migrations in that order — they are the same steps the deploy workflow
runs. Do not run individual migrates/migrate_*.py scripts: they are
historical, unregistered, and several drop tables. scripts/migrate.py is the
only supported path, and it records what it applied in the schema_migrations
ledger so it can be verified afterwards.
docker compose run --rm --no-deps executes the migration against the
shared database in a one-off container (removed on exit, no port bindings,
no dependency restarts) — the migration scripts set SKIP_SCHEMA_GUARD=1
themselves, so the guard (enforced at application startup via
ensure_schema_compatible()) stays active for normal application runs
and never runs migrations for you. Never start web before its required
schema exists; the guard failing fast is the system protecting you from a
half-migrated deploy.
Chat memory (instance/chat_memory.db, WAL mode) persists on the chat_memory
volume across restarts and rebuilds.
⚙️ Environment Variables
Copy
.env.example→.envand fill in.
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
✅ | Flask session secret (32+ chars) |
DATABASE_URL |
✅ (local dev) | postgresql://user:pass@host:5432/db — auto-built from POSTGRES_* in Docker |
TMDB_API_KEY |
✅ | TMDb API |
OPENAI_API_KEY |
✅ | OpenAI API key (chat + embeddings) |
GOOGLE_CLIENT_ID |
✅ | Google OAuth 2.0 |
GOOGLE_CLIENT_SECRET |
✅ | Google OAuth 2.0 |
CLOUDINARY_CLOUD_NAME |
✅ | Avatar uploads |
CLOUDINARY_API_KEY |
✅ | Cloudinary |
CLOUDINARY_API_SECRET |
✅ | Cloudinary |
CLOUDINARY_URL |
⭕ | Cloudinary (alternative single-var config) |
STREAM_PROVIDER |
⭕ | Default embed source: rive | vidking | vidy | oneembed |
RATELIMIT_STORAGE_URI |
⭕ | Redis URI for multi-worker rate limits (defaults to memory) |
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD |
✅ (Docker) | Self-hosted DB credentials |
POSTGRES_EXPOSED_PORT |
⭕ | Loopback-bound host port for the episode-sync workflow |
WEB_WORKERS / WEB_THREADS |
⭕ | Gunicorn sizing (defaults 2 / 4 — do not raise blindly) |
FLASK_DEBUG |
⭕ | Never true in production |
MAIL_SERVER / MAIL_* |
⭕ | Password reset emails |
RATELIMIT_STORAGE_URI |
⭕ | Redis for rate limiting (defaults to memory) |
📁 Project Structure
FrameIQ/
├── app.py # Application factory (create_app)
├── models/ # SQLAlchemy models package (user, media, chat, …)
├── extensions.py # Flask extensions (limiter, mail, db)
├── requirements.txt
│
├── routes/ # 28 Flask blueprints (one per domain)
│ ├── auth.py # Login, register, password reset
│ ├── main.py # Home, search, news, tonights-pick
│ ├── details.py # Movie / TV detail pages
│ ├── reviews.py # Reviews & ratings
│ ├── diary.py # Watch diary
│ ├── lists.py # Custom lists (basic + advanced)
│ ├── tv_tracking.py # Episode & season tracking
│ ├── chat.py # SSE streaming chat
│ ├── social.py # Follow, activity feed
│ ├── stats.py # Dashboard, year-in-review
│ └── ...
│
├── src/
│ ├── agents/ # LangGraph multi-agent system
│ │ ├── graph.py # StateGraph definition
│ │ ├── nodes.py # Supervisor, Retriever, Chat, Enricher
│ │ ├── tools.py # 7 LangChain tools (cached TMDb + history)
│ │ └── state.py # GraphState schema
│ └── api/
│ ├── agent_service.py # User context building, SSE streaming
│ └── flask_integration.py
│
├── api/ # Shared utilities
│ ├── tmdb_client.py # TMDb wrapper (compat shim)
│ ├── tmdb/ # TMDb package: cache, movies, tv, people, search
│ ├── stream_providers.py # Embed providers (Rive, VidKing, Vidy, 1Embed)
│ └── chatbot.py # LLM helpers
│
├── templates/ # 47 Jinja2 templates (unified on base.html)
├── static/
│ ├── css/ # tokens.css, chrome.css, detail.css, projector.css
│ ├── js/ # chat-page.js, chrome.js, projector-dust.js
│ └── images/
│
├── scripts/ # Ops (excluded from Docker)
│ ├── sync_upcoming_episodes.py
│ └── collect_media.py
│
├── migrates/ # Manual schema migrations
├── .github/workflows/ # ci-cd.yml, deploy.yml, sync-upcoming-episodes.yml
├── nginx/nginx.conf # Reverse proxy + SSL
├── docker-compose.yml
├── Dockerfile
└── Makefile
🔄 CI/CD Pipeline
| Workflow | Trigger | Actions |
|---|---|---|
| ci-cd.yml | Push to main/develop |
offline pytest + flake8 |
| deploy.yml | Push to main (after CI pass) |
SSH → VPS → make deploy |
| sync-upcoming-episodes.yml | Daily 02:00 UTC | Sync TMDb → PostgreSQL |
VPS: Self-hosted PostgreSQL 16, Docker Compose, system nginx, nightly pg_dump backups, certbot SSL.
Health: /health (app) and /agent_health (chat graph loaded?) — make status hits the latter.
🧪 Testing & Quality
The ordinary test suite is offline: no TMDB_API_KEY, no DNS, no internet.
# Full offline suite (the default, and what CI runs) pytest tests/ # Smoke only pytest tests/test_basic.py # Models pytest tests/test_models.py # Single test pytest -k "test_name"
How the offline boundary works
Every TMDb payload is served from deterministic fixtures, so tests exercise the real cache, fetchers, parsers and resolvers against fake payloads rather than against a live service:
| Concern | Where |
|---|---|
| Fixture payloads + fake transport | tests/tmdb_offline.py |
| Network guard + per-test isolation | tests/conftest.py |
| Shared deterministic TV fixtures | tests/tv_fixtures.py |
| The contract itself (pinned) | tests/test_tmdb_offline.py |
requests is intercepted at requests.sessions.Session.send, so an
unregistered TMDb endpoint returns the real "invalid API key" payload and any
other host raises immediately. Raw sockets, socket.getaddrinfo,
httpx and urllib are blocked too — an accidental request fails loudly
with Unexpected network access during offline test: <url> rather than
silently reaching api.themoviedb.org.
TMDb integration tests (opt-in, needs a real key)
Genuine external verification lives behind a marker and is deselected by default:
pytest -m tmdb # real TMDb; requires TMDB_API_KEY in the env pytest -m "not tmdb" # the offline suite (same as plain `pytest tests/`)
Browser/data verification scripts under scripts/ (verify_tv_*_browser.py)
are a separate, manual layer: they use a configured TMDb key and are never
part of pytest tests/.
Quality gates: offline suite green with no TMDb secret • flake8 clean • vulture clean • 0 secrets • 0 dead code
🤝 Contributing
We welcome contributions! Please read CLA.md before submitting a PR.
Good first issues:
- 🌍 Add more languages/regions to TMDb discover tools
- 🧪 Write missing unit tests in
tests/ - ♿ Improve accessibility (ARIA, keyboard nav)
- 📈 Add chart types to stats dashboard
- 🎨 Extend design token system
# 1. Fork & clone git clone https://github.com/YOUR-USERNAME/FrameIQ.git # 2. Branch git checkout -b feat/your-feature # 3. Develop # ... make changes ... # 4. Verify pytest tests/ && uv run flake8 . # 5. Push & PR git push origin feat/your-feature # Open PR against main
📄 License
Licensed under GNU Affero General Public License v3.0 — see LICENSE.
Commercial use, SaaS hosting, or white-labelling requires a separate licence.
Contact:robinmill4d@gmail.com
🙏 Acknowledgements
- TMDb — The movie database powering our data
- LangGraph — Multi-agent orchestration
- Letterboxd — Product inspiration
- Archivo • Inter • JetBrains Mono — Typefaces
- Tailwind CSS • Chart.js • Lucide — Frontend toolkit
Built with ❤️ by Robin Millford and contributors.
Star ⭐ the repo if you find it useful — it helps more people discover FrameIQ.
来源:GitHub · github.com