Skip to main content

Sentence-graph context management for multi-agent AI pipelines

Project description

sentex

Context management middleware for multi-agent AI pipelines.

pip install sentex

You have agents. They produce outputs. The next agent needs to read those outputs — but not all of them. Just the parts that are relevant to its specific task, within a token budget.

Sentex is the layer between your agents that makes this work.


The problem

In any multi-agent pipeline, Agent 3 ends up carrying the full output of Agents 1 and 2 — whether it's relevant or not. At scale this blows token budgets and degrades output quality. Every framework either dumps everything in or makes you manage it manually.

How Sentex works

Every agent output goes into a shared sentence graph. When the next agent runs, Sentex retrieves exactly the sentences relevant to its task — traversing semantic KNN edges across agent boundaries — and delivers them within a token budget you set.

Agent 1 output → split into sentences → embedded → KNN graph
                                                         ↓
Agent 3 query → find entry point → BFS traversal → 8 sentences (not 60)

Quickstart — bring your own agents

from sentex import ContextGraph

graph = ContextGraph()

# === Your Agent 1 runs (any framework — LangChain, CrewAI, raw API) ===
output = my_agent_1.run("research the immune system")
graph.put("search-results", output, agent_id="researcher")

# === Build context for Agent 2 ===
context = graph.get("search-results", query="write a 60-second script", budget=2000)
# context = [most relevant sentences from Agent 1's output]

prompt = f"Use this research:\n{chr(10).join(context)}\n\nWrite the script."
script = my_agent_2.run(prompt)

graph.put("script", script, agent_id="writer")
graph.used("search-results")   # boosts retrieval for future runs

Three calls: put(), get(), used(). That's the integration.


The four context levels

Every node in the graph has four representations:

Level What When to use
L0 ~50-token identity sentence Scanning many nodes to decide which to load
L1 Sentence-graph retrieval Default — get the relevant sentences, nothing else
L2 ~300-token extractive summary When you need a coherent overview, not fragments
L3 Full raw content When the agent needs everything
# L1 — sentence graph (default)
context = graph.get("search-results", query="immune cells", budget=2000)
# → ["T-cells recognize antigens on pathogen surfaces.",
#    "Antigen recognition triggers the adaptive immune cascade.", ...]

# L2 — extractive summary (first ~300 tokens of content)
summary = graph.get("search-results", query="", budget=2000, layer="l2")
# → "The immune system has two branches: innate and adaptive. T-cells..."

# L3 — full content
full = graph.get("search-results", query="", budget=99999, layer="l3")
# → the entire raw output from Agent 1

# L0 — identity (first sentence, used by scan_nodes)
identity = graph.get("search-results", query="", budget=100, layer="l0")
# → "The immune system has two branches: innate and adaptive."

Multi-agent example

from sentex import ContextGraph

graph = ContextGraph()

# Agent 1: researcher
research = researcher_agent.run("immune system")
graph.put("resources/research", research, agent_id="researcher")

# Agent 2: analyst — only sees the research sentences relevant to its task
context = graph.get("resources/research", query="key mechanisms and findings", budget=1500)
analysis = analyst_agent.run(f"Analyse:\n{chr(10).join(context)}")
graph.put("working/analysis", analysis, agent_id="analyst")
graph.used("resources/research")

# Agent 3: writer — graph edges cross agent boundaries automatically.
# Sentences from research and analysis that are semantically close get KNN edges.
# BFS traversal for each node surfaces the most relevant from both.
research_ctx  = graph.get("resources/research", query="write a script", budget=1000)
analysis_ctx  = graph.get("working/analysis",   query="write a script", budget=800)

script = writer_agent.run(
    f"Research:\n{chr(10).join(research_ctx)}\n\n"
    f"Analysis:\n{analysis_ctx}\n\n"
    f"Write a 60-second video script."
)
graph.put("working/script", script, agent_id="writer")

Structured assembly (optional)

If you want Sentex to handle budget enforcement across multiple reads automatically:

from sentex import ContextGraph, defineAgent, Read

graph = ContextGraph()

# ... agents 1 and 2 have run, graph has content ...

writer_manifest = defineAgent(
    id="writer",
    reads=[
        Read("resources/research", layer="l1", budget=1500),
        Read("working/analysis",   layer="l2", budget=500),
    ],
    writes=["working/script"],
    token_budget=4000,      # total cap across all reads
    fallback="l2",          # if L1 confidence < 0.5, serve L2 instead
)

assembled = graph.assemble_for(writer_manifest, query="write a video script")

# assembled.context         → {"resources/research": [sentences], "working/analysis": "summary"}
# assembled.token_count     → 1923   (enforced before the LLM call — never a surprise)
# assembled.layers_used     → {"resources/research": "l1", "working/analysis": "l2"}
# assembled.confidence      → {"resources/research": 0.84, "working/analysis": 1.0}
# assembled.compressed      → []   (nothing fell back due to budget)
# assembled.missing         → []   (all declared reads were available)

# Build a prompt from the assembled context:
sections = []
for node_id, content in assembled.context.items():
    text = "\n".join(content) if isinstance(content, list) else content
    sections.append(f"[{node_id}]\n{text}")
prompt = "\n\n".join(sections) + "\n\nWrite a 60-second video script."

script = writer_agent.run(prompt)
graph.put("working/script", script, agent_id="writer")
graph.mark_used(assembled, used_ids=["resources/research"])

assemble_for() handles:

  • Token budget across all reads (total cap, not per-read)
  • Automatic fallback: L1 → L2 → L0 if over budget
  • Confidence-based fallback: low similarity → serve L2 instead of bad sentences
  • Full diagnostics on what was actually served

Dynamic node discovery (AutoRead)

When you don't know the node IDs at definition time — scan all nodes at L0, retrieve from the top-k:

from sentex import ContextGraph, AutoRead, defineAgent

graph = ContextGraph()

# ... many nodes ingested under resources/* ...

manifest = defineAgent(
    id="synthesiser",
    reads=[AutoRead(top_k=3, layer="l1", budget_per_node=1000, scope="resources")],
    writes=["working/synthesis"],
    token_budget=5000,
)
assembled = graph.assemble_for(manifest, query="immune system mechanisms")
# → scans all resources/* nodes at L0
# → retrieves L1 from the 3 most relevant
# → keys in assembled.context: "auto:resources/research", "auto:resources/docs", ...

Graph inspection

graph.stats()
# → {"nodes": 3, "sentences": 47, "edges": 235, "edge_boosts": 12, "node_ids": [...]}

graph.get_node("resources/research")
# → ContextNode(id=..., produced_by=..., sentence_ids=..., l0=..., l2=..., ...)

# Scan nodes by relevance (L0 level — fast, no sentence retrieval)
graph.scan_nodes("immune cell coordination", top_k=3)
# → [("resources/research", 0.87), ("working/analysis", 0.74), ...]

HTTP server (for TypeScript / non-Python pipelines)

uvicorn sentex.server:app --port 8765
POST /put        body: {node_id, content, agent_id}
POST /get        body: {node_id, query, budget, layer}
POST /used       body: {node_ids: [...]}
POST /scan       body: {query, top_k, scope}
POST /assemble   body: {agent_id, reads, token_budget, query}
GET  /nodes      list all nodes with token counts
GET  /nodes/{id} inspect a single node
GET  /health     node count, sentence count

OpenAPI docs at http://localhost:8765/docs.


Pipeline decorator (optional orchestration)

If you want Sentex to own the run loop rather than just managing context:

from sentex import Pipeline, Read

pipeline = Pipeline()

@pipeline.agent(id="researcher", writes=["resources/research"])
async def researcher(ctx):
    return await ctx.llm("Research the immune system in detail.")

@pipeline.agent(
    id="writer",
    reads=[Read("resources/research", layer="l1", budget=2000)],
    writes=["working/script"],
    token_budget=4000,
)
async def writer(ctx):
    return await ctx.llm(ctx.render() + "\n\nWrite a 60-second video script.")

result = await pipeline.run(
    query="explain how the immune system works",
    llm="gpt-4o",   # any LiteLLM model string
)

print(result.outputs["working/script"])
print(result.summary())

Cross-run memory (SQLite-backed)

Edge weights and node summaries persist between runs so retrieval improves over time:

from sentex import Pipeline, MemoryStore

store = MemoryStore("./sentex.db")
pipeline = Pipeline(persist="./sentex.db")

# Run 1: baseline retrieval
# Run 2: L2 summaries cached, no regeneration needed
# Run 3+: session history available

store.all_sessions()
# → [{"session_id": ..., "query": ..., "started_at": ..., "committed_at": ...}, ...]

How the sentence graph works

When content is ingested:

  1. Split into sentences (NLTK Punkt; code blocks and lists are atomic)
  2. Each sentence embedded (all-MiniLM-L6-v2, 384 dims, runs locally, no API key needed)
  3. K nearest neighbours computed across all sentences in the graph — not just within the current node. Sentences from different agents get edges automatically.
  4. Extractive L0 (first sentence) and L2 (first ~300 tokens) built immediately; LLM-generated summaries are optional and async.

When an agent retrieves at L1:

embed(query) → highest-sim sentence in this node (entry point)
             → BFS via KNN edges (crosses agent boundaries)
             → collect until token budget hit
             → return sorted by relevance

What Sentex does not do

  • No agent orchestration — bring your own (LangChain, CrewAI, AutoGen, anything)
  • No vector database required (numpy handles the KNN)
  • No graph database required (Python dict handles adjacency)
  • No cloud infrastructure (runs in-process; SQLite for optional persistence)
  • No opinion on which LLM you use (any LiteLLM-compatible model for L0/L2 generation)

Comparison

LangChain LlamaIndex Sentex
Sentence-level retrieval No No Yes
Cross-agent graph edges No No Yes
Works with any agent framework Yes Yes Yes
Budget enforcement pre-call No No Yes
Requires vector DB No Yes No
Runs fully in-memory Yes No Yes
HTTP server for polyglot pipelines No No Yes

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sentex-0.2.4.tar.gz (60.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sentex-0.2.4-py3-none-any.whl (52.0 kB view details)

Uploaded Python 3

File details

Details for the file sentex-0.2.4.tar.gz.

File metadata

  • Download URL: sentex-0.2.4.tar.gz
  • Upload date:
  • Size: 60.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for sentex-0.2.4.tar.gz
Algorithm Hash digest
SHA256 4c9b7e94ad36fe9cda3757e4e1e8bfc212727b975545026be453cdc67550193d
MD5 f59c42c9cf3f937dfc92f580e608655a
BLAKE2b-256 558da3c138a2545bcedb8ccb85d4b0782cb922d51695721223d774cf757106ef

See more details on using hashes here.

File details

Details for the file sentex-0.2.4-py3-none-any.whl.

File metadata

  • Download URL: sentex-0.2.4-py3-none-any.whl
  • Upload date:
  • Size: 52.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for sentex-0.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 39e670c301cec3da7f4ca07bd4497c8545006368f693fe5a146011b84bb6ddbd
MD5 10b22dbee369d7e0f8c0c0a2eff071a8
BLAKE2b-256 f3596fd091ad26642692785f89c0251bee9a69c8bdcd1f1446defcad0aa869fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page