Oh My Notion is a Python MVP for a local-first Notion agent. It prioritizes local search over synced Notion content, then uses the retrieved evidence to answer questions.
It is designed as a learning project for agent development:
- local-first retrieval before any remote model call
- grounded answers based on retrieved Notion evidence
- optional OpenAI summarization layer on top of local search
- hybrid retrieval with FTS + local semantic embeddings + rerank
- Local SQLite storage
- SQLite FTS5 full-text search
- Local embedding index for semantic retrieval
- Local file ingestion for
.mdand.txt - Local-first agent flow
- CLI entrypoint for
init-db,ingest-sample,ingest-files,search,ask,sync,reindex, andserve - Real Notion sync for pages, child pages, and database entries
- Lightweight web frontend served by Python
- Optional OpenAI API integration for grounded answer generation
- Local inspection commands for raw snapshots, parsed pages, chunks, links, and stats
- Local eval commands for retrieval and agent tool routing
The project now uses a source -> tool -> skill -> planner -> runtime -> context -> answer architecture.
The agent reads from multiple local knowledge sources:
- Notion snapshots in
data/raw/*.json - Local files in
knowledge_files/ - SQLite memory tables for session memory, facts, preferences, and summaries
- Structured link records extracted from Notion pages and local files
Main code:
app/notion/sync.pyapp/notion/parser.pyapp/ingestion/files.pyapp/storage/db.py
All sources are normalized into the same local storage model:
pageschunkschunk_embeddingssaved_linkssession_turnsmemory_factsmemory_preferencesmemory_summaries
This keeps Notion pages and local files inside the same retrieval plane.
Main code:
app/storage/models.pyapp/storage/db.pyapp/storage/reindex.py
Tools are the smallest executable capabilities. They do not decide the full task flow; they only fetch or update data.
Current tools include:
search_local_notionlist_recent_pagesget_pagesearch_saved_linkslist_top_link_domainsfind_pages_by_domainget_link_domain_summaryread_network_linklookup_memorylookup_preferencessave_memorysave_preference
Main code:
app/retrieval/tools.pyapp/agent/memory.pyapp/agent/tools_registry.py
Skills are task-oriented orchestration units built on top of tools. A skill may call one or more tools and may decide whether a follow-up step is needed.
Current skills:
local_qa_skillmulti_source_research_skilllink_research_skillmemory_skillrecent_activity_skillgeneric_research_skill
Main code:
app/skills/registry.py
The planner is now skill-first.
It decides:
- which skill should handle the question
- which arguments should be passed to the skill
- whether the next step should stop or continue based on observations
There are two planner modes:
- LLM-driven skill planning when OpenAI is available
- rule-based skill routing as fallback
Main code:
app/agent/planner.py
The runtime executes a thought -> skill -> tool -> observation loop with:
- step limits
- duplicate-call prevention
- follow-up tool execution
- planner trace recording
- session persistence
- automatic memory capture
Main code:
app/agent/runtime.pyapp/agent/executor.py
The context builder turns retrieved evidence, memory, network results, and planner trace into a bounded final context for the LLM.
It handles:
- layered token budgets
- retrieval / memory / network / trace allocation
- dynamic trimming
- citation ids
- explanation output
Main code:
app/context/builder.pyapp/context/models.py
The final answer layer has two modes:
- OpenAI grounded generation using the built context
- local evidence-first fallback when OpenAI is unavailable
Main code:
app/llm.pyapp/agent/rendering.pyapp/agent/service.py
The project includes built-in tooling for:
- retrieval evaluation
- agent routing evaluation
- local snapshot inspection
- dashboard analysis
Main code:
app/evaluation/app/inspection/app/webapp/server.py
A normal ask request runs roughly like this:
- The planner selects a skill.
- The skill expands into one or more tool calls.
- Tools return observations from local knowledge, links, memory, or network content.
- The runtime decides whether to stop or continue another step.
- The context builder assembles the final evidence bundle.
- The answer layer returns either:
- an OpenAI grounded answer
- or a local fallback answer
- The session is persisted and memory may be updated.
In short:
Question
-> Planner
-> Skill
-> Tool Calls
-> Observations
-> Context Builder
-> Answer
-> Memory Update / Eval
python -m venv .venv
source .venv/bin/activate
pip install -e .
oh-my-notion init-db
oh-my-notion ingest-sample
oh-my-notion ingest-files
oh-my-notion search "agent"
oh-my-notion ask "我之前写过哪些和 agent routing 有关的内容?"oh-my-notion serveThen open http://127.0.0.1:8000.
Create a local config file first:
cp .env.example .envThen edit .env and run:
oh-my-notion sync
oh-my-notion reindex
oh-my-notion recent
oh-my-notion ask "我关于 agent routing 的笔记写了什么?"The app loads configuration from .env in the project root. Shell environment variables still work and override values from .env.
Example .env:
NOTION_TOKEN=secret_xxx
NOTION_ROOT_PAGE_ID=your-root-page-id
NOTION_VERSION=2022-06-28
OPENAI_API_KEY=sk-xxx
OPENAI_MODEL=gpt-4.1-miniSupported keys:
OH_MY_NOTION_DB_PATH: optional path to SQLite databaseOH_MY_NOTION_KNOWLEDGE_DIR: optional path to the local files directory, defaults toknowledge_files/NOTION_TOKEN: your Notion integration tokenNOTION_ROOT_PAGE_ID: root page ID to start recursive syncNOTION_VERSION: optional Notion API version, defaults to2022-06-28OPENAI_API_KEY: enables grounded answer generation with OpenAIOPENAI_MODEL: OpenAI model name, defaults togpt-4.1-mini
If NOTION_VERSION is missing or invalid, the app falls back to 2022-06-28.
When OPENAI_API_KEY is configured, ask and the web UI will:
- search the local Notion index first
- send only the retrieved evidence to OpenAI
- generate a concise Chinese answer grounded in those snippets
If the API key is missing or the API call fails, the app falls back to the local evidence template.
If you only changed parsing or cleaning rules, you usually do not need to call Notion again.
oh-my-notion reindexThis rebuilds the local SQLite index from data/raw/*.json.
It also regenerates chunk embeddings for hybrid retrieval and rebuilds local files from knowledge_files/.
You can also rebuild only matching raw files:
oh-my-notion reindex notesPut local .md or .txt files under:
knowledge_files/
Then ingest them into the same local index:
oh-my-notion ingest-files
oh-my-notion search "agent"
oh-my-notion ask "我在本地文件和 Notion 里关于 agent 写了什么?"The file source shares the same chunks, embeddings, hybrid retrieval, and agent pipeline as Notion pages.
These commands help inspect local data without calling Notion again:
oh-my-notion inspect-raw <page-id-or-file>
oh-my-notion inspect-page <page-id-or-file>
oh-my-notion inspect-chunks <page-id-or-file>
oh-my-notion inspect-links <page-id-or-file>
oh-my-notion statsUse the built-in eval dataset or provide your own JSONL file:
oh-my-notion eval-retrieval
oh-my-notion eval-agent
oh-my-notion eval-allDefault dataset location:
eval/questions.jsonl
Each JSONL item can include:
questionexpected_pagesexpected_tools
The project now uses a lightweight Hybrid RAG pipeline:
- FTS5 for keyword recall
- local hashed embeddings for semantic recall
- score merge + rerank before answer generation
This keeps the system fully local by default. Rebuild the index once after upgrading:
oh-my-notion reindex
oh-my-notion search "agent routing"
oh-my-notion ask "我关于 agent routing 的笔记写了什么?"- Extend parsing coverage for more Notion block types
- Add incremental sync based on
last_edited_time - Add an LLM answer layer on top of the current evidence-first flow