The Answer Was Already in Teams. We Just Couldn't Find It.

Somewhere in your company's chat history is the exact fix for the problem you are debugging right now. Someone wrote it down two years ago, in a thread, at 11pm. Good luck finding it.
That was the problem I set out to solve at ASI Solutions. Our knowledge was not missing. It was scattered. Decisions lived in Microsoft Teams. Delivery history lived in Linear. Deal context lived in HubSpot. Documents lived in SharePoint. Meeting notes lived in Granola. Proposals lived in PandaDoc. Invoices lived in Xero. Code and pull requests lived in GitHub.
The standard answer is a wiki. Pick a tool, tell everyone to migrate their knowledge into it, and watch it go stale within a quarter. I have seen that fail enough times to stop believing in it. People do their work where their work happens, and no mandate changes that.
So I built AKMS, the ASI Knowledge Management System, on the opposite premise. Meet the knowledge where it lives. Connectors read from the tools people already use, and nobody has to change how they work. People search it from the agents and editors they already have, through MCP, and get a cited answer drawn from everything the company has already written down.
Credit where it belongs
I did not invent this architecture. Cerebras Systems published a write-up of their internal knowledge base, How we built our knowledge base, and it is one of the most useful engineering posts I have read. I modelled AKMS on it directly, including their hybrid retrieval design. The ideas I copied, in plain language:
- Every source lands in one embeddings table in Postgres. A Teams conversation, a Linear issue, a HubSpot deal, and a SharePoint document all become the same row shape. Adding a source means writing a connector, not a new query path.
- Never embed raw chat. A transcript embeds badly because greetings and thread drift land in the same vector as the answer. An LLM distills each conversation into the question someone would actually search for, plus a summary and the resolution, and that is what gets embedded. The raw transcript stays available for exact-text search.
- Rescue the message buried in the thread. A 200-message incident channel flattens into a few sentences, and the one message that explains the fix disappears. So substantial runs from a single author are indexed separately, if they clear a quality bar. Otherwise the index fills with "sounds good, thanks!".
- Trust no single scorer. Full-text search catches exact tokens. Vector search catches paraphrase. Rare-token matching catches error codes and hostnames. Recency matters because infrastructure answers expire. The four ranked lists are fused by rank, reranked, and the final answer carries a citation on every claim.
Where I differ from Cerebras is not for lack of hardware. We have RTX GPUs, including NVIDIA H100 and 200s. They embed at 3,072 dimensions. I stayed at 768 because that was enough for this corpus. I rerank with a cross-encoder rather than a small LLM. And I added connectors they never described, because Teams, HubSpot, Granola, PandaDoc, and Xero are where we actually work.
Everyone uses it through MCP
There is a web UI. It is for admins. Allowlists, status, keys. Nobody asks AKMS a question there.
Every user searches through MCP. Cursor, Claude Code, Microsoft Copilot, our own internal agents. Any MCP-capable system can call it. The knowledge stays in the tools people already work in.
I made one design decision that matters here. The MCP server returns evidence, not answers. Its five tools (search, search_code, who_knows, recent_prs, list_projects) run retrieval only. There is no synthesis model on the server. Rows come back with titles, text, links, and timestamps, and the agent on the client side reads that evidence and writes the answer, citing every source. Whatever model you already trust in your editor is the one that reasons over the results.
The stronger models do something else with those links. They do not stop at the snippet AKMS returned. They follow the URL into the original Teams conversation, the Linear issue, the document, and read the raw source. The index finds the trail. The model walks it, and puts the story together from the material itself.
What sits under the search
The corpus is one Postgres table. pgvector holds the 768-dimension embeddings. A GIN index covers a weighted full-text column. Title ranks highest, distilled text next, raw transcript last. That is why an error string in a Teams message is still findable, without that filler polluting the vector index. Nearest-neighbour search uses HNSW.
I did not add a broker. Ingest jobs, sync cursors, memberships, and the audit log live in the same database, with row-locked job claims so workers can race without double-processing. One dump is a complete backup.
A question does not run one search. A planner model chooses which retrievers to fire, and they run in parallel. Full-text for exact tokens. Cosine similarity for paraphrase. Inverse document frequency for rare tokens, error codes, hostnames, ticket ids. Recency, because an infrastructure answer from last year is often wrong. Reciprocal Rank Fusion then merges those lists by rank, not by score. A document three retrievers agree on beats one that got lucky in a single list. After fusion, a cross-encoder reads the query and each surviving document as a pair. That is what demotes a hit that shares vocabulary with the question but answers a different one. The keepers get context expanded back in, neighbouring document chunks or the rest of the thread, before MCP returns the rows.

Code is its own path. I split files on language-aware definition boundaries so a function stays whole, signature, body, and docstring together. GitHub's compare API means I re-embed only what changed. Hits come back with line-accurate permalinks. who_knows ranks people by authorship of the high-signal rows, not by who talks the most.
Each source is a connector that emits the same row shape. The query stack never learns that a Xero invoice is not a Teams thread. Adding a source is a connector, not a second retrieval pipeline.
Security throughout, not bolted on
An internal knowledge base concentrates everything sensitive a company has, so I put the gates in from day one.
Nothing is indexed until it is allowlisted. A single config file names every channel, chat, site, repo, pipeline, and tenant I index, and connectors check that list before anything is queued. Content outside the curated corpus never reaches the database at all.
Production sits behind Cloudflare Access, tied to our Entra identity. Every MCP key is issued per person. Every query lands in an audit log. Sources carry their own access control, and Xero is restricted from day one. Only people explicitly granted membership on the financial source can see invoices and bills. Everyone else gets nothing, not even a hint that the rows exist.
What it actually does for people
AKMS is organizational memory. "Have we seen this error before?" finds the incident thread from last year. "Who knows about our SharePoint sync?" ranks the people who wrote the substantive answers, with links to what they wrote. "What did we agree in that meeting?" finds the Granola notes. Sales can pull deal history. Finance can check an invoice without leaving their editor. Engineers get recent pull requests and indexed code with line-accurate links.
None of this required anyone to write documentation. The documentation was already there, disguised as work.
Why I built it this way
The goal was never a product. AKMS is internal, and the constraint that shaped it is that ASI handles customer environments, so no customer or internal data leaves the building. Inference runs on our own GPUs. Postgres with pgvector stores the corpus, the vectors, the sync cursors, and the audit log, so one database dump is a complete backup. No third-party inference API. No data egress.
If you are sitting on years of Teams chats, tickets, and meeting notes, my advice is the same advice the Cerebras post gave me. Do not migrate the knowledge. Index it where it lives, distill the chat instead of embedding it raw, combine several retrievers instead of trusting one, and put identity, allowlists, and audit in before the first document is indexed. The answers are already written down.