Enterprise RAG usually blends a knowledge base (Confluence) with tickets (Jira). Both expose webhooks, so freshness is event-driven. The pattern: stamp artifact_id from each page/issue during retrieval, and fire the matching change from the webhook payload.
1. Retriever — derive ids from the source
Your Confluence + Jira content lives in a vector DB. This is a plain vector lookup, so extend BaseVectorRetriever — implement search (your client's query) and to_chunk (map one hit, deriving the id from its metadata):
from coalent import BaseVectorRetriever, Chunk
class KnowledgeRetriever(BaseVectorRetriever):
def __init__(self, vector, embed):
self._vector, self._embed = vector, embed
def search(self, query, namespace):
return self._vector.search(self._embed(query), top_k=6)
def to_chunk(self, hit):
# metadata already carries the source + id + revision:
# {"source": "confluence", "id": "98231", "version": "7", "text": "..."}
return Chunk(
artifact_id=f"{hit['source']}:{hit['id']}", # confluence:98231 / jira:OPS-412
text=hit["text"],
version=str(hit["version"]),
)
(Not a plain vector lookup? Implement the Retriever interface directly — just a class with a retrieve method, as in the MCP/tools and vector-cache examples.)
2. Wire webhooks → invalidate
The bundled event layer turns native payloads into change events. Jira has a built-in connector; add a tiny one for Confluence. Both derive the id from the payload:
from coalent import EventDispatcher, JiraConnector, ChangeEvent
from coalent.events import EventConnector
class ConfluenceConnector(EventConnector):
source = "confluence"
def parse(self, payload):
page = payload.get("page") or {}
if not page.get("id"):
return []
return [ChangeEvent(
artifact_id=f"confluence:{page['id']}", # dynamic — from the webhook
version=str(page.get("version", {}).get("number", "")),
kind="delete" if payload.get("event") == "page_removed" else "update",
)]
dispatcher = EventDispatcher(
sink=cache.invalidate, # feed events straight to the cache
connectors=[JiraConnector(), ConfluenceConnector()],
)
JiraConnector emits jira:<KEY> from an issue payload — the same scheme your retriever stamped, so they line up.
3. Receive the webhook
@app.post("/webhooks/{source}")
def receive(source: str, payload: dict):
events = dispatcher.dispatch(source, payload) # parses + invalidates
return {"invalidated": len(events)}
Point Confluence at /webhooks/confluence and Jira at /webhooks/jira. A page edit or an issue update now dirties exactly the cognition units that used it; a page deletion evicts them.
Use Confluence's version.number and Jira's fields.updated as the version — Coalent then skips no-op events where the revision didn't actually move.
Next
- End-to-end — everything in one app.
- Provenance & freshness — the connector + event model.