This release is a pre-release and may not be stable for production use.
cyrrus
Context routing for LLM calls.
Instead of sending a full static system prompt every time, cyrrus decides what context is relevant right now and sends only that.
from cyrrus import Projector
from cyrrus.providers import ollama
bot = Projector.minimal(llm_call=ollama("llama3.2"))
reply = bot.ask("hello")
That's it. No config required to start.
What it does
Every time a user sends a message, cyrrus:
- Routes — matches the message against your slides to find what's relevant
- Remembers — pulls stored facts about this user that relate to this message
- Packs — fits the relevant context into your token budget, highest priority first
- Sends — builds a proper messages array and calls your LLM
- Learns — extracts new facts from the conversation and stores them for later
The LLM receives clean, relevant context. Not a wall of everything you've ever defined.
Numbers
- 51.6% token reduction on average across mixed conversations (real tiktoken counts)
- 0.018 MB core install — zero required dependencies
- 121.9 MB / 241.7 MB RAM for the optional embeddings block
- 500 concurrent users with zero errors
- Surviving every adversarial test thrown at it
Install
pip install cyrrus # core, zero dependencies
pip install cyrrus[embeddings] # + semantic routing and memory
The import is always:
from cyrrus import Projector
Three ways to use it
Zero config
from cyrrus import Projector
from cyrrus.providers import ollama
bot = Projector.minimal(llm_call=ollama("llama3.2"))
reply = bot.ask("write me a python function")
With context routing
config = {
"core_lamp": {
"content": "You are a helpful assistant.",
},
"code_lens": {
"content": "Output only clean code blocks, no filler.",
"triggers": ["code", "script", "python", "function", "bug"],
},
"casual_lens": {
"content": "Keep it short and natural.",
"triggers": ["hey", "hi", "hello"],
},
}
bot = Projector(config, llm_call=ollama("llama3.2"))
reply = await bot.process("write a sorting function", session_id=str(user_id))
tokens and priority are optional — cyrrus fills them in automatically.
Individual components
from cyrrus import MemoryVault, IntentRouter, SlideTray
# Use any component standalone
memory = MemoryVault()
facts = await memory.retrieve("user_123", "what is my name", limit=3)
Projector also works as an async context manager — background fact extraction is awaited and cleaned up on exit:
async with Projector(config, llm_call=ollama("llama3.2")) as bot:
reply = await bot.process("hello", session_id=str(user_id))
# or manage the lifecycle yourself
bot = Projector(config, llm_call=ollama("llama3.2"))
reply = await bot.process("hello", session_id=str(user_id))
await bot.aclose()
Providers
Ready-made wrappers for common APIs. Pass them directly as llm_call:
from cyrrus.providers import ollama, openai, anthropic, groq
# Local Ollama — free, no API key
bot = Projector(config, llm_call=ollama("llama3.2"))
# OpenAI
bot = Projector(config, llm_call=openai("gpt-4o-mini", api_key="sk-..."))
# Anthropic
bot = Projector(config, llm_call=anthropic("claude-haiku-4-5", api_key="sk-..."))
# Groq — fast, generous free tier
bot = Projector(config, llm_call=groq("llama-3.1-8b-instant", api_key="..."))
Or bring your own — any async function that takes a messages list and returns a string:
async def my_llm(messages: list) -> str:
response = await client.chat.completions.create(
model="my-model",
messages=messages,
)
return response.choices[0].message.content
bot = Projector(config, llm_call=my_llm)
Memory
cyrrus remembers things users tell it. No setup needed — the default extractor runs automatically:
User: "my name is Muratha" → stored: user_name = Muratha
User: "I'm building cyrrus" → stored: user_project = cyrrus
User: "I prefer Ollama" → stored: user_preference = Ollama
On the next message, relevant facts are retrieved and injected into the system prompt automatically.
With cyrrus[embeddings]: retrieval uses semantic similarity, so "what am I working on" finds user_project: cyrrus even without shared keywords.
Without: keyword overlap matching. Fast, zero dependencies, catches most cases.
Memory is per session_id. Facts never leak between users.
Conversation history
cyrrus maintains a sliding window of conversation history per session. The LLM always sees recent context, so follow-up questions work:
User: "what's the capital of France?"
Bot: "Paris."
User: "what's the population there?" ← LLM knows "there" means Paris
Bot: "Around 2.1 million in the city proper."
What cyrrus is not
- Not an agent framework. It controls input, never output.
- Not a vector database. Memory is SQLite with semantic retrieval optional.
- Not a replacement for LangChain. It's a layer, not a framework.
- Not magic. Keyword routing misses natural phrasing. Install
cyrrus[embeddings]if you need paraphrase matching.
session_id
This is important. Every user needs a unique session_id:
# Discord bot
reply = await bot.process(message.content, session_id=str(message.author.id))
# Web app
reply = await bot.process(user_input, session_id=request.session["user_id"])
# Single-user script — use ask() and don't worry about it
reply = bot.ask("hello")
Without a unique session_id, users share memory. That's a bug, not a feature.
Chat adapter
cyrrus.adapters.make_chat() builds a single chat(session_id, user_text)
coroutine you can drop into a Discord/Slack/Telegram bot in two lines.
15-line Discord bot example:
import discord, os, asyncio
from cyrrus import Projector
from cyrrus.adapters import make_chat
from cyrrus.providers import ollama # or openai / anthropic / groq
SLIDES = {"core_lamp": {"content": "You are a friendly assistant."}}
client = discord.Client(intents=discord.Intents.default())
bot = Projector(SLIDES, llm_call=ollama("llama3.2"))
chat = make_chat(bot)
@client.event
async def on_message(m):
if m.author == client.user or not m.content.startswith("!"):
return
reply = await chat(str(m.author.id), m.content[1:].strip())
await m.channel.send(reply[:2000])
asyncio.run(client.start(os.environ["DISCORD_TOKEN"]))
Tracing
Every call stores a trace:
await bot.process("write a script", session_id="u1")
trace = bot.last_trace
print(trace["routed_slide_ids"]) # which slides matched
print(trace["memory_slide_ids"]) # which facts were retrieved
print(trace["dropped_slide_ids"]) # what didn't fit the budget
print(trace["messages"]) # exact messages sent to LLM
print(trace["stats"]["history_turns"]) # how many turns of history
License
AGPLv3. If you run cyrrus as part of a network service, you must make your modifications available. See LICENSE.
cyrrus — formerly known as slid3s
Metadata
Release files for cyrrus 0.3.0b2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cyrrus-0.3.0b2.tar.gz | 102.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cyrrus-0.3.0b2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 155.4 kB
Release files / cyrrus-0.3.0b2.tar.gz
| Download URL | cyrrus-0.3.0b2.tar.gz |
|---|---|
| Size | 102.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aef52fe5eb7fa58eaa9f1d7d7cee69dde5e76733a6615ba250d25c630c4b928f
|
|
BLAKE2b-256 checksum How to use checksums |
2f51a71760489a2590563d998075612bcb89b696ecb92f4eb55b5659854d8434
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Release files / cyrrus-0.3.0b2-py3-none-any.whl
| Download URL | cyrrus-0.3.0b2-py3-none-any.whl |
|---|---|
| Size | 53.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c31c3d5f00a0d762f3b05131558af4f5fb640903ce0b0c2ccfa8dce4e0752d48
|
|
BLAKE2b-256 checksum How to use checksums |
6b6adbcc817b8ddde99e9973188b4ee76e8f11b0de73b5590027c859c65e4a14
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|