OpenViking Content Agent is a lightweight, self-hosted AI content automation platform. It reads technical articles from a local OpenViking knowledge base, generates reviewable content, and publishes through platform plugins.
flowchart LR
KB["OpenViking knowledge base"] --> K["Knowledge adapter"]
K --> A["Content agent"]
C["config.yaml"] --> A
S["Dynamic skills"] --> A
L["OpenAI-compatible LLM API"] --> A
A --> M["Memory JSONL"]
A --> R["Human review queue"]
R --> P["Publisher plugins"]
P --> B["Blog Markdown"]
P --> W["WeChat"]
P --> X["X"]
P --> XHS["Xiaohongshu"]
backend/
app/
agents/ # Agent orchestration
api/ # FastAPI routes
config/ # config.yaml loading and validation
domain/ # Core models
knowledge/ # OpenViking adapter
llm/ # LLMProvider interface
memory/ # Memory and skill proposal storage
publishers/ # Publisher plugins
scheduler/ # Cron jobs
skills/ # Declarative skill plugins
config/
tests/
knowledge/
data/
uv sync --extra dev
cp config/config.yaml config/local.yaml
export DEEPSEEK_API_KEY="your-deepseek-api-key"
OPENVIKING_AGENT_CONFIG=config/local.yaml uv run uvicorn backend.app.main:app --reloadFor a no-credentials demo with sample knowledge and a deterministic local LLM:
bash scripts/demo.shThis uses config/demo.yaml, reads examples/knowledge/, writes reviewable drafts to
data/demo-memory.jsonl, and keeps all publishers in dry-run mode.
Run the local acceptance check before publishing changes:
bash scripts/check.shThe check runs the test suite, Ruff, demo config validation, OpenViking ingestion, status, publisher checks, generation, and review queue listing. You can pass another config path:
bash scripts/check.sh config/local.yamlThe same acceptance check runs in GitHub Actions on pushes and pull requests.
Then open:
curl http://localhost:8000/api/health
curl http://localhost:8000/api/status
curl http://localhost:8000/api/config/summaryGenerate content:
curl -X POST http://localhost:8000/api/knowledge/ingest
curl -X POST http://localhost:8000/api/generate \
-H "Content-Type: application/json" \
-d '{"content_type":"daily_summary","platforms":["blog","wechat","x","xiaohongshu"]}'Review and approve content:
curl http://localhost:8000/api/review
curl "http://localhost:8000/api/review?status=pending_review&platform=x"
curl -X POST http://localhost:8000/api/review/<content_id>/approve
curl -X POST http://localhost:8000/api/review/<content_id>/reject \
-H "Content-Type: application/json" \
-d '{"reason":"Needs more source detail before publishing."}'Publish approved content:
curl http://localhost:8000/api/publishers/x/check
curl http://localhost:8000/api/publish/x/<content_id>/validate
curl -X POST http://localhost:8000/api/publish/x/<content_id>Edit config/config.yaml, then run one startup command:
docker compose up --buildMount your OpenViking export or vault at ./knowledge.
The container runs OpenSkald validate-config before starting the API and exposes
a Docker healthcheck at /api/health.
All deploy-time choices live in config/config.yaml:
- OpenAI-compatible LLM base URL, model, and API key env var
- OpenViking knowledge base path
- Scheduler cron jobs
- Scheduled OpenViking ingestion, generation, and approved-content publishing
- Publisher accounts and credentials env vars
- Human review and memory storage paths
No API keys or publishing accounts should be hardcoded in code.
Default scheduled generation includes:
- Daily Summary
- Weekly Summary
- Hot Topic Analysis
- Deep Technical Analysis
OpenViking articles are indexed by the ingest_knowledge schedule into
memory.article_index_path. Approved content is picked up by the publish_approved
schedule.
Generation uses the indexed articles first. If the index is empty, the agent falls back to the configured OpenViking path. If both are empty, generation fails with a clear error instead of creating empty drafts.
/api/config/summary returns a redacted runtime summary:
- Secret values are never returned.
- Secret environment variable names are shown so operators can fix deployment.
- Missing OpenViking paths and unsafe production settings are reported as issues.
/api/healthreportsdegradedwhen configuration warnings or errors exist.
The CLI uses the same services as the FastAPI app and prints JSON for automation:
uv run OpenSkald --config config/local.yaml validate-config
uv run OpenSkald --config config/local.yaml config-summary
uv run OpenSkald --config config/local.yaml status
uv run OpenSkald --config config/local.yaml knowledge-ingest
uv run OpenSkald --config config/local.yaml generate-once \
--content-type daily_summary \
--platform blog \
--platform x \
--platform wechat
uv run OpenSkald --config config/local.yaml knowledge-list --query rag
uv run OpenSkald --config config/local.yaml review-list --status pending_review
uv run OpenSkald --config config/local.yaml content-summary
uv run OpenSkald --config config/local.yaml content-failures
uv run OpenSkald --config config/local.yaml memory-timeline --limit 10
uv run OpenSkald --config config/local.yaml memory-search --query retrieval
uv run OpenSkald --config config/local.yaml skills-discover
uv run OpenSkald --config config/local.yaml review-approve --content-id <content_id>
uv run OpenSkald --config config/local.yaml publisher-check --platform x
uv run OpenSkald --config config/local.yaml publisher-check-all
uv run OpenSkald --config config/local.yaml publish-content --content-id <content_id>
uv run OpenSkald --config config/local.yaml publish-approved --platform xIf publishing fails, the content remains approved and the error is stored in
metadata.last_publish_error. Fix credentials or platform permissions, then retry the same
content_id.
Failure inspection:
uv run OpenSkald --config config/local.yaml status
uv run OpenSkald --config config/local.yaml content-summary
uv run OpenSkald --config config/local.yaml content-failures --platform x
curl http://localhost:8000/api/status
curl http://localhost:8000/api/content/summary
curl "http://localhost:8000/api/content/failures?platform=x"Memory inspection:
uv run OpenSkald --config config/local.yaml knowledge-ingest
uv run OpenSkald --config config/local.yaml knowledge-list --query "openviking"
curl -X POST http://localhost:8000/api/knowledge/ingest
curl "http://localhost:8000/api/knowledge/search?q=openviking"
uv run OpenSkald --config config/local.yaml memory-timeline --platform x
uv run OpenSkald --config config/local.yaml memory-search --query "review edit"
curl "http://localhost:8000/api/memory/timeline?platform=x&limit=10"
curl "http://localhost:8000/api/memory/search?q=review%20edit"Create a folder under backend/app/skills/<skill_name>/skill.yaml.
Skills are declarative prompt modules and are loaded at startup.
Skill proposals are human-gated. Approved proposals create disabled drafts:
curl -X POST http://localhost:8000/api/skills/proposals \
-H "Content-Type: application/json" \
-d '{
"title":"Architecture comparison writer",
"reason":"Repeated architecture comparison posts need a reusable prompt.",
"proposed_skill_name":"architecture_comparison_writer",
"draft_prompt":"Compare these articles and produce a practical architecture note.\n\n{articles}",
"content_types":["deep_technical_analysis"],
"platforms":["wechat"]
}'
curl http://localhost:8000/api/skills/proposals
curl -X POST http://localhost:8000/api/skills/proposals/<proposal_id>/approve \
-H "Content-Type: application/json" \
-d '{"note":"Draft only. Review prompt before enabling."}'Generated skill drafts are written with enabled: false; they are never auto-executed.
The agent can also discover proposals from memory:
uv run OpenSkald --config config/local.yaml skills-discover
curl -X POST http://localhost:8000/api/skills/proposals/discoverDiscovery only creates pending proposals. A human must approve a proposal before a disabled
skill draft is materialized, and the draft remains enabled: false until edited deliberately.
Create backend/app/publishers/<platform>/publisher.py with a PluginPublisher class.
The core orchestration does not need to change.
Each publisher can implement validate(content) to enforce platform rules before a
post is published. Validation failures are stored in content metadata and do not
change the item to published.
Built-in publishers:
blog: writes Markdown files to the directory configured byaccount_id.wechat: uses WeChat Official Account APIs whendry_run: false.x: posts an X thread through the X API whendry_run: false. Requires OAuth 1.0a User Context credentials, or an OAuth 2.0 User Context token, with write/post permission forPOST /2/tweets.xiaohongshu: uses an experimental creator-web cookie adapter whendry_run: false. Verify with a real note publish before relying on it for unattended production.
Production credential env vars must be JSON objects:
export X_PUBLISHER_CREDENTIALS='{"api_key":"YOUR_CONSUMER_KEY","api_key_secret":"YOUR_CONSUMER_SECRET","access_token":"YOUR_ACCESS_TOKEN","access_token_secret":"YOUR_ACCESS_TOKEN_SECRET"}'
export WECHAT_PUBLISHER_CREDENTIALS='{"app_id":"wx_xxx","app_secret":"xxx","thumb_media_id":"xxx"}'
export XIAOHONGSHU_PUBLISHER_CREDENTIALS='{"cookie":"YOUR_CREATOR_COOKIE"}'For X, app permissions must be Read and Write, and Access Token and Secret must be regenerated after changing permissions. App-only bearer tokens are rejected because they cannot publish user tweets.
X live-send checklist:
export X_PUBLISHER_CREDENTIALS='{"api_key":"YOUR_CONSUMER_KEY","api_key_secret":"YOUR_CONSUMER_SECRET","access_token":"YOUR_ACCESS_TOKEN","access_token_secret":"YOUR_ACCESS_TOKEN_SECRET"}'
uv run OpenSkald --config config/config.yaml validate-config
uv run OpenSkald --config config/config.yaml publisher-check --platform x
uv run OpenSkald --config config/config.yaml generate-once \
--content-type daily_summary \
--platform x
uv run OpenSkald --config config/config.yaml review-list \
--status pending_review \
--platform x
uv run OpenSkald --config config/config.yaml review-approve --content-id <content_id>
uv run OpenSkald --config config/config.yaml publish-content --content-id <content_id>API equivalent:
curl http://localhost:8000/api/publishers/x/check
curl http://localhost:8000/api/publishers/checks
curl -X POST http://localhost:8000/api/review/<content_id>/approve
curl -X POST http://localhost:8000/api/publish/x/<content_id>For production, keep enabled: false until credentials and platform permissions are verified.
The app validates production config and reports missing credentials before startup checks pass.
- Keep
review.require_human_approvalenabled for publishing accounts. - Run behind a reverse proxy with TLS.
- Store secrets in environment variables or a secret manager.
- Persist
./dataon durable storage. - Start with the
blogpublisher and dry-run external publishers. - Enable WeChat/X/Xiaohongshu one platform at a time after real account-level tests.
- Keep generated
data/and localknowledge/out of Git unless publishing sample fixtures.
See CONTRIBUTING.md for development workflow and SECURITY.md for credential and
vulnerability reporting guidance. The repository includes an MIT license; maintainers
should confirm it matches the intended release policy before publishing.