Metadata-Version: 2.4
Name: cyrrus
Version: 0.2.0
Summary: Context routing for LLM calls. Decides what goes into the prompt so you don't have to send everything every time.
License: AGPL-3.0
Project-URL: Homepage, https://github.com/kikusuka/cyrrus
Project-URL: Issues, https://github.com/kikusuka/cyrrus/issues
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: embeddings
Requires-Dist: fastembed; extra == "embeddings"
Requires-Dist: numpy; extra == "embeddings"
Provides-Extra: facts-onnx
Requires-Dist: onnxruntime>=1.16.0; extra == "facts-onnx"
Requires-Dist: gliner2-onnx>=0.1.0; extra == "facts-onnx"
Requires-Dist: numpy; extra == "facts-onnx"
Provides-Extra: facts-torch
Requires-Dist: gliner>=0.2.5; extra == "facts-torch"

# 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.

```python
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:

1. **Routes** — matches the message against your slides to find what's relevant
2. **Remembers** — pulls stored facts about this user that relate to this message
3. **Packs** — fits the relevant context into your token budget, highest priority first
4. **Sends** — builds a proper messages array and calls your LLM
5. **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

```bash
pip install cyrrus                  # core, zero dependencies
pip install cyrrus[embeddings]      # + semantic routing and memory
```

The import is always:
```python
from cyrrus import Projector
```

---

## Three ways to use it

### Zero config

```python
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

```python
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

```python
from cyrrus import MemoryVault, IntentRouter, SlideTray

# Use any component standalone
memory = MemoryVault()
facts = await memory.retrieve("user_123", "what is my name", limit=3)
```

---

## Providers

Ready-made wrappers for common APIs. Pass them directly as `llm_call`:

```python
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:

```python
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`:

```python
# 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.

---

## Tracing

Every call stores a trace:

```python
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
