A lookup service for your company's AI assistants: each one gets only the facts it may see, with the source of each.
Try it after cloning: make demo opens it in your browser. No API key, no Docker.
Companies that run several AI assistants (programs that answer questions or propose actions using a language model) usually connect each one to the company's databases on its own. Each connection re-implements who may see what. When an assistant quotes a price or a policy, nobody can tell which record it came from or how old it was.
Agentic Context Service is one HTTP service your assistants ask instead. Your databases send it each change once. When an assistant asks a question, the service first removes everything that assistant is not allowed to see, then searches what is left, and returns short passages. Each passage names its source record, version, and age. You run it on your own machines.
Technical docs: Architecture · Getting started for developers · API contract · Threat model
Watch the 1080p walkthrough · 30-second Safe Local Agents overview
You need git, make, and uv. uv installs Python 3.13 for you
if needed. uv's install script needs curl. On a bare Linux image also install
ca-certificates; without it, git clone and the uv download fail.
git clone https://github.com/gainratio/agentic-context-service.git
cd agentic-context-service
make demoThe first run downloads Python and about 110 packages (roughly 200 MB). On a fast connection that takes seconds; allow a minute or two on a slow one. Then your browser opens http://127.0.0.1:8090/demo/. On a machine with no browser, such as a server or a container, it prints that address for you to open instead. Press Ctrl+C in the terminal to stop it.
The page tells a short story. Demo Retail is a made-up shoe shop with two AI assistants: one helps its pricing team, one answers customers at the help desk. A second shop, Other Retailer, uses the same service. Between them the service holds seven records: pricing rules, a product fact, a return policy, an approved sale, a finance note, and a board plan. Pick a scenario:
- The pricing assistant asks for a discount limit. The 20% rule comes back first, with where
it came from: record
NS-100in the catalog database, version 7, two hours old. The finance note and the board plan are set aside. - The customer help assistant asks the same discount question. It does not get the internal 20% rule. It gets the approved 15% sale, which the help desk may quote.
- Another shop's assistant asks the same discount question. It gets only its own shop's rule.
- The customer help assistant asks about returns. It gets the return policy, which only the help desk may read.
- The customer help assistant tries to open pricing rules. Refused with
403 POLICY_DENIED. Asking for more than its job allows never gets it more.
For each one the page shows, in plain words, the question, what the service did, what the assistant got back, why it matters, and what happened to each of the seven records. A "For developers" panel shows the exact HTTP request and response and lets you ask your own question.
The page says which mode it runs in. make demo is the offline demo: the HTTP API, the
signature check, and the access rules are the real service code, but records live in memory and
are matched by words, and the access rules from
policies/context/data.json are applied by a small built-in
stand-in for Open Policy Agent. CI checks that the stand-in gives the same answers as the real
Open Policy Agent. Nothing leaves your computer. The full stack below swaps in
OpenSearch, live database changes, and the real Open Policy Agent.
Prefer the terminal? This prints a smaller version of the same check, run in-process with no browser and no server:
uv run python examples/quickstart/governed_retrieval.pypublished 3 records: a pricing rule, a finance-only note, another company's rule
pricing-analysis asks "What is the discount limit for NS-100?" -> 1 result(s)
NS-100 may be discounted at most 20% without pricing-lead approval.
source: postgres://catalog/NS-100 v7, 90s old, trust: verified
customer-support asks "What is the discount limit for NS-100?" -> 0 result(s)
Your assistant calls the service over HTTP: one POST /v1/context:retrieve per question. Any
language works (the signature recipe is below). In Python you need an HTTP client and, for local
testing, this package's request signer. The package is the whole server, about 50 dependencies,
so in production you may prefer to compute the signature yourself:
pip install httpx "agentic-context-service @ git+https://github.com/gainratio/agentic-context-service"With make demo running, this is a complete pricing-assistant client
(examples/client/ask_the_service.py):
"""Ask the service for context the way your AI assistant would.
Start the service with `make demo`, then in a second terminal run:
uv run python examples/client/ask_the_service.py
"""
import asyncio
import time
import uuid
import httpx
from agentic_context_service.api.request_context import RequestContextSigner
SERVICE_URL = "http://127.0.0.1:8090"
# The demo's signing key. In production your API gateway holds the key and signs for you.
SIGNING_KEY = b"offline-demo-signing-key-not-for-production"
def signed_headers() -> dict[str, str]:
headers = {
"Authorization": "Bearer pricing-assistant-token", # who is asking
"X-Team-ID": "pricing",
"X-App-ID": "pricing-assistant",
"X-Workflow-ID": "price-check",
"X-Workflow-Revision": "v1",
"X-Agent-ID": "pricing-assistant",
"X-Environment": "development",
"X-Cost-Center": "retail",
"X-Request-ID": str(uuid.uuid4()),
"X-Trace-ID": uuid.uuid4().hex,
"X-ACS-Timestamp": str(int(time.time())),
}
headers["X-ACS-Signature"] = RequestContextSigner(SIGNING_KEY).sign(headers)
return headers
async def ask(client: httpx.AsyncClient, question: str) -> list[dict]:
response = await client.post(
"/v1/context:retrieve",
headers=signed_headers(),
json={
"query": question,
"purpose": "pricing-analysis", # the job this answer is for
"corpora": ["pricing", "product", "approved-decisions"],
"filters": {},
"retrieval": {
"mode": "lexical",
"candidate_limit": 20,
"result_limit": 3,
"max_context_tokens": 2000,
},
},
)
response.raise_for_status()
return response.json()["results"]
async def main(client: httpx.AsyncClient) -> None:
for result in await ask(client, "What is the discount limit for NS-100?"):
citation = result["citation"]
print(result["text"])
print(f" from {citation['source_uri']} version {citation['source_version']}")
async def run() -> None:
async with httpx.AsyncClient(base_url=SERVICE_URL) as client:
await main(client)
if __name__ == "__main__":
asyncio.run(run())It prints:
NS-100 discount limit: at most 20% off list price without pricing-lead approval.
from postgres://catalog/pricing_rules/NS-100 version 7
NS-100 is a waterproof trail running shoe, sizes 6 to 13, list price $120.
from postgres://catalog/products/NS-100-product version 3
Approved promotion: NS-100 is 15% off until October 31. Support may quote this price.
from postgres://decisions/promotions/PROMO-88 version 1
What to know when you wire it into a real assistant:
- The headers say who is asking and for which job. The bearer token identifies the
assistant. The service decides what it may see from the token and the
purpose, never from anything else the caller claims. - The signature normally comes from your API gateway. In production your gateway holds the
key, so assistants never see it. The demo key above is only for the demo. In any language:
take these 11 headers in this order,
authorization,x-team-id,x-app-id,x-workflow-id,x-workflow-revision,x-agent-id,x-environment,x-cost-center,x-request-id,x-trace-id,x-acs-timestamp; write each asname:value(lowercase name, trimmed value); join them with\n; and send the hex HMAC-SHA256 of that text asX-ACS-Signature. The timestamp is Unix seconds. The service refuses a signature older than five minutes or more than 30 seconds in the future. The shared key must be at least 32 bytes. - Each result carries its evidence.
citationnames the source system, record, and version;freshnessgives its age;trust_classsays how much to trust it. Put these in the assistant's prompt so its answer can cite them. - Refusals are explicit. Every error body is
{"code": ..., "message": ..., "request_id": ..., "retryable": ...}.400 INVALID_REQUESTmeans the body did not match the contract,401 UNAUTHENTICATEDmeans the identity or signature failed,403 POLICY_DENIEDmeans the assistant asked for something its job does not allow, and409 CONTEXT_STALEmeans you setstale_behavior: "fail"and a match was older than yourmax_age_seconds.
The API contract lists every route and field, and the
architecture guide explains them. The service can also store
and search assistant memory (/v1/memories). A test runs this exact file against the service,
so the code and output above cannot drift.
For developers exploring the repository layout and policy stack:
src/agentic_context_service/: FastAPI routes, bearer/HMAC request verification, OPA policy adapter, OpenSearch retrieval adapter, memory endpoints, and the offline browser demo.policies/: Open Policy Agent (Rego) rules and fixture data tested in CI.connectors/&deploy/: Debezium PostgreSQL change-log config, Redpanda queue, OpenSearch, and local Compose stack.packages/contracts/openapi.yaml&.env.example: full HTTP schema and local configuration defaults (cp .env.example .env).
flowchart LR
S["Company databases<br/>(PostgreSQL, the source of truth)"] -->|each change published once| I["Indexer<br/>chunk, embed, version"]
I --> O["Search index<br/>(OpenSearch)"]
A["AI assistant"] -->|one signed request| C["Context Service<br/>permissions first, then ranking"]
C --> O
C -->|cited, dated answers| A
classDef blue fill:#e8f4f8,stroke:#7aa7b8,color:#171717
classDef green fill:#e8f8e8,stroke:#7ab87a,color:#171717
classDef orange fill:#f8f0e8,stroke:#b8987a,color:#171717
classDef purple fill:#f0e8f8,stroke:#9a7ab8,color:#171717
class S orange
class I,O blue
class C green
class A purple
Changes in PostgreSQL are picked up from the database's change log (Debezium) and passed through a queue (Redpanda) to an indexer. The indexer splits each record into chunks and stores them in OpenSearch with the record's version, so a late or replayed change never overwrites a newer one and a deleted record stays deleted. An assistant sends one signed request that says which company it works for and what job it is doing. A policy engine (Open Policy Agent) turns that into filters that are applied before any ranking, and a caller's own filters can narrow them but never widen them. OpenSearch then ranks what is left by exact words and by meaning, and the service returns the top passages with their sources and ages.
- It is Beta. Version 0.1.0 has no tagged release and is not published to PyPI. You install it from this repository.
- It is not a chat app or a framework for building assistants. It is the lookup your assistant calls.
- It knows one assistant out of the box. The stock entry point (
bootstrap.py) accepts a single token,ACS_DEMO_TOKEN, and has no setting for more. To give each of your assistants its own identity, implementBearerAuthenticatorfromapi/request_context.py(for example, one that validates your identity provider's tokens) and pass it tocreate_app(authenticator=...)from your own entry point. - It is not the source of truth. Your databases are. The index is updated a moment after the database changes, not in the same transaction.
- The local stack is for trying it, not for production. It turns off TLS, private networking, and OpenSearch's per-document security so it runs on a laptop. There is no high availability, backup and restore, or load testing yet. See the roadmap.
- The offline demo and the terminal quickstart match words only. Search by meaning needs the full stack below. The offline demo also cannot store assistant memory.
- It cannot protect you from a compromised server or operator, or from what an AI provider
does with data you choose to send it. Nothing leaves your machines unless you turn on the
optional live AI mode (
ACS_SHOWCASE_AGENT_MODE=deep) in the full-stack dashboard.
| If you have | Use |
|---|---|
| One assistant and one data source | Let the assistant query that source directly |
| Documents only, and no per-user or per-team access rules | A vector database with a retrieval library |
| A need for a managed service, and you accept its data handling | A hosted enterprise search product |
| Several assistants, access rules, and a need to trace every answer to a record | Agentic Context Service |
This runs PostgreSQL, Debezium, Redpanda, OpenSearch, OPA, the API, and the indexer in Docker.
You need Docker with Compose v2, at least 4 GiB of memory for Docker, and make. make up
checks Docker's memory first and stops with a clear message if it is too small; set
ACS_SKIP_MEMORY_CHECK=1 to try anyway. Every image is published for amd64 and arm64, so Apple
silicon runs it without emulation.
cp .env.example .env
make bootstrap
make up
make seed
make stack-demomake stack-demo walks through a pricing question with citations, a database update showing up
in search, an assistant being refused, and saved assistant memory. It exits with an error if any
step does not behave as expected. While the stack runs, a live dashboard is at
http://localhost:8080/showcase/. Stop it with make down. Details are in
operations.
Run make bootstrap (uv sync --all-extras --all-groups) first so every optional adapter and test dependency is installed before make verify:
make bootstrap
make verifymake verify runs the same checks as CI: lint, strict type checks, tests with at least 90%
coverage, security scans, and a retrieval-quality evaluation. It takes about 20 seconds. The tests
ignore any .env file, so it is safe to run after the quickstart above. make ui runs the
browser tests for the demo page and the dashboard.
Getting started for developers covers prerequisites, a map of the
code, and a walkthrough of a first change.
- Architecture: the data path, API shape, security model, configuration, and what the tests prove.
- Getting started for developers: from clone to a green build and your first change.
- Five-minute architecture: the design rules on one page.
- Interactive architecture map: a guided visual tour.
- Specification: the full engineering contract and acceptance criteria.
- Operations: health checks and what to do when something breaks.
- Threat model: what is protected and what is not.
- Design decisions: one short record per decision.
- API contract: the OpenAPI definition.
- Examples: the client above, the terminal quickstart, and two AI agent examples where code, not the model, approves the action.
- Demo source: the offline demo's records, assistants, and page.
- CONTRIBUTING.md, SECURITY.md, CHANGELOG.md, and ROADMAP.md.
MIT. See LICENSE.
