Architecture

Notes go in through a watcher and a job queue; questions come back as citations with line ranges. Each box names the file that implements it.

real: always part of the system optional: needs a language model dashed arrow: reads or writes the database

On a narrow screen, read the table of every part instead of the drawings.

The two flows

knowman architecture, the ingestion and query flows INGESTQUERYENTRY POINTS Postgres + pgvectorstore.py is the only module that speaks SQL: chunks (embedding column, HNSW index), jobs, eval runs, traces jobsreads jobschunks + vectors k nearest nothing within distance question Notes (.md)CORPUS_PATH folderREAL Watcherwatcher.pywatchfiles eventsREAL Job queuestore.pyingest / delete jobsREAL Workerworker.pyclaims jobsREAL Chunkerchunking.pyingest.pyREAL Embeddingsembeddings/ollama.pyOllamaREAL Embed queryretrieval.pyembeddings/ollama.pyREAL Nearest chunksstore.pyHNSW, cosineREAL Distance filterretrieval.pydefault max 0.5REAL Citationsretrieval.pypath:Lstart-LendREAL LLM answerask.pyllm/*.pyOPTIONAL Evaleval.pydataset.jsonREAL No evidence"No evidence foundfor that query."REAL Agentagent.pyknowman writeOPTIONAL CLIcli.pyknowman ...REAL HTTP APIapi.py/search, /ask, /evalREAL MCP servermcp_server.pysearch, ask (stdio)REAL Dashboarddashboard.pyGET /dashboardREAL

Retrieval, citations and eval need only Postgres and Ollama for embeddings. A language model adds the written answer in ask. It runs under Docker Compose or natively with systemd units; a GPU is used through Vulkan when /dev/dri exists and is never required. Source: core/README.md, core/docs/running.md.

The components behind them

The same system, grouped by layer. Read it top to bottom: each layer calls the one below it, and the bottom row is used by all of them. Every box is a file or a package under core/src/knowman/.

knowman components by layerEntry points: CLI, HTTP API, MCP server, dashboard. Services: watcher, worker, optional agent. Use cases: ingest, ask, eval. Core: chunking, retrieval, formatting. Adapters: embeddings, language model providers, prompts. Storage: store module over Postgres with pgvector. Cross-cutting: config, access guard, logging. Arrows point from a layer to the layer it depends on. ENTRY SERVICES USE CASES CORE ADAPTERS STORAGE SHARED CLIcli.pyknowman search, ask, ingestREAL HTTP APIapi.py/search, /ask, /eval, /indexREAL MCP servermcp_server.pysearch and ask over stdioREAL Dashboarddashboard.pyGET /dashboardREAL Watcherwatcher.pywatchfiles events become jobsREAL Workerworker.pyclaims one job at a timeREAL Agentagent.py, LangGraphknowman write: searches, then writesOPTIONAL Ingestingest.pyfile to chunks, vectors, rowsREAL Askask.pyanswer written from the citationsOPTIONAL Evaleval.pydataset scored against retrievalREAL Chunkingchunking.pysplit at blank lines, keep the linesREAL Retrievalretrieval.pyembed, k nearest, filter, citeREAL Formattingformatting.pypath:L5-L6, or the explicit negativeREAL Embeddingsembeddings/base.py, ollama.pyone vector per chunk, same modelREAL Language modelsllm/base.py + ollama, claude,openai, grokOPTIONAL Promptsprompts/ask_v1.txtPROMPT_VERSION picks the fileOPTIONAL Storestore.py: the only module that speaks SQLPostgres + pgvector: chunks (embedding column, HNSW cosine index), jobs, eval runs, tracesREAL Configconfig.pyone Settings from env and .envREAL Access guardaccess.pytoken and rate limit, both offREAL Logginglogging_setup.pyone JSON record per lineREAL

The arrow on the left runs from the layer that calls to the layer it depends on. Config, the access guard and logging are drawn last because every layer above reads them. Source: core/src/knowman/, core/README.md, core/docs/running.md.

Every part

Names in the Part column are translated. Names in the Code column are the real files and settings.

PartCodeStatusWhat it does
Notes (.md)CORPUS_PATH folderrealYour Markdown folder. Never modified by indexing; only the agent writes to it.
Watcherwatcher.pyrealTurns each added, edited or deleted .md file into a job.
Job queuestore.pyrealJobs are rows in Postgres: ingest_path and delete_path, with status pending, processing, done or failed.
Workerworker.pyrealClaims one job at a time and runs it; a failed job is recorded, not retried silently.
Chunkerchunking.py, ingest.pyrealSplits at blank lines, keeping each chunk's line range; ingest.py embeds and stores the chunks.
Embeddingsembeddings/base.py, embeddings/ollama.pyrealOllama turns each chunk into a vector (model set by EMBEDDINGS_MODEL). One interface, one implementation.
Postgres + pgvectorstore.pyrealStores chunks, jobs, eval runs and traces. HNSW cosine index on the embedding column.
Embed queryretrieval.pyrealEmbeds the question with the same model.
Nearest chunksstore.pyrealReturns the k closest chunks (default k from RETRIEVAL_DEFAULT_K).
Distance filterretrieval.pyrealDrops chunks farther than RETRIEVAL_MAX_DISTANCE. An empty result is the explicit negative.
Citationsretrieval.pyrealEach citation carries path, line start, line end, text and distance.
Formattingformatting.pyrealRenders a citation as path:Lstart-Lend plus its passage, and an empty result as "No evidence found for that query."
LLM answerask.py, llm/*.pyoptionalOnly when LLM_PROVIDER is set (Ollama, Claude, OpenAI or Grok) and there is evidence. search and eval never need it.
Promptsprompts/ask_v1.txtoptionalThe template the answer is written with; PROMPT_VERSION selects the file, so a new wording is a new file, not a code change.
Evaleval.py, evaldata/dataset.jsonrealScores a labeled dataset against retrieval; no model judges another model.
Agentagent.pyoptionalknowman write: a LangGraph agent over Ollama that can search the notes and write a new one.
CLIcli.pyrealdb-init, ingest, search, ask, eval, worker, watch, mcp, write.
HTTP APIapi.pyrealSearch, ask, eval, index jobs, status; optional API_TOKEN and rate limit.
Access guardaccess.pyrealThe token check and the per-minute request counter behind those two options. Both are off unless set, and neither knows what search or ask do.
MCP servermcp_server.pyrealTwo read-only tools over stdio: search and ask.
Dashboarddashboard.pyrealOne page served at /dashboard: index vs disk, job queue, last eval, recent activity.
Configconfig.pyrealOne Settings object read from the environment and .env; every module asks it instead of parsing variables.
Logginglogging_setup.pyrealJSON logs on stdout, one record per line, so the containers and the systemd units log the same shape.