AI contextual assistant with few-shot learning and context injection. Provider-agnostic, framework-agnostic.
Project description
melissa-sage 🌿
AI contextual assistant with few-shot learning and context injection. Provider-agnostic. Framework-agnostic. Pure Python.
What is it
melissa-sage helps you explain on-screen data to users using real values, not generic explanations. It combines two techniques:
- Few-shot learning — YAML example pairs that teach the model HOW to respond (tone, formatting, structure)
- Context injection — Jinja2 templates that take cached data and render it as WHAT the model should explain, including business rules and formulas
It is provider-agnostic (Anthropic, OpenAI, OpenRouter, self-hosted), framework-agnostic (Django, FastAPI, Celery, scripts), and has zero framework dependencies.
Quickstart
pip install melissa-sage[anthropic]
from melissa_sage import Melissa
melissa = Melissa.from_config("melissa_sage.yaml")
melissa.cache_data("salarios", {"employees": [...]}, context_key="tenant_1")
result = melissa.explain("salarios", context_key="tenant_1")
print(result["reply"])
Configuration (melissa_sage.yaml)
assistant:
provider: anthropic
model: claude-sonnet-4-20250514
api_key: ${ANTHROPIC_API_KEY}
max_tokens: 1024
cache:
backend: redis # or "memory" for dev
url: ${REDIS_URL}
prefix: msa
ttl: 3600
prompt:
system: |
You are an assistant that explains data the user is viewing on screen.
Always use the exact values from the injected context.
sections:
salarios:
label: Salarios Base
examples: prompts/salarios_examples.yaml
template: prompts/salarios_template.yaml
required_keys:
- employees
Few-shot examples
Few-shot examples teach the model the exact format, tone, and structure you want.
# prompts/salarios_examples.yaml
examples:
- user: |
[Context: Sofía Mendoza | Sr. Executive | $15,800]
Explain this
assistant: |
💰 **Salary for Sofía Mendoza**
Sofía earns a base monthly salary of **$15,800 MXN**.
Examples are emitted as user/assistant conversation turns before the real question, so the model has seen the format before it has to produce one.
Jinja2 templates
Templates take cached data and render it as the context message sent to the model. Four custom filters are available:
| Filter | Example |
|---|---|
money |
$145,000 |
money2 |
$145,000.00 |
pct |
5% |
num |
145,000 |
# prompts/salarios_template.yaml
content: |
[Context — Salaries — Period: {{ periodo }}]
{% for e in employees %}
• {{ e.nombre }}: {{ e.salario | money }} MXN
{% endfor %}
Providers
| Provider | YAML key | Default model |
|---|---|---|
| Anthropic | anthropic |
claude-sonnet-4-20250514 |
| OpenAI | openai |
gpt-4o |
| OpenRouter | openrouter |
anthropic/claude-sonnet-4 |
| Custom | custom |
llama3.2 (Ollama by default) |
Switching providers is a single line in the YAML:
assistant:
provider: openai
model: gpt-4o
api_key: ${OPENAI_API_KEY}
Cache
| Backend | Use case |
|---|---|
memory |
Dev, tests, scripts |
redis |
Production |
The context_key parameter is deliberately neutral — you decide whether it represents a tenant, user, session, page, or a composite scope. The full cache key format is {prefix}:{context_key}:{section_id}.
Examples
Standalone script
from melissa_sage import Melissa
melissa = Melissa.from_config("melissa_sage.yaml")
melissa.cache_data(
"salarios",
{"employees": [{"nombre": "Sofía", "puesto": "Sr.", "salario": 15800}]},
context_key="demo",
)
print(melissa.explain("salarios", context_key="demo")["reply"])
Django
# views.py
def cache_salarios(request):
employees = Employee.objects.filter(org=request.user.org)
melissa.cache_data(
"salarios",
{"employees": [e.as_dict() for e in employees]},
context_key=str(request.user.org_id),
)
return JsonResponse({"ok": True})
def explain_salarios(request):
result = melissa.explain(
"salarios",
context_key=str(request.user.org_id),
question=request.GET.get("q"),
)
return JsonResponse(result)
FastAPI (async + streaming)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.get("/explain/{section_id}")
async def explain(section_id: str, tenant: str, q: str | None = None):
result = await melissa.aexplain(section_id, context_key=tenant, question=q)
return result
@app.get("/explain-stream/{section_id}")
async def explain_stream(section_id: str, tenant: str):
async def gen():
async for chunk in melissa.aexplain_stream(section_id, context_key=tenant):
if chunk["type"] == "text":
yield chunk["content"]
return StreamingResponse(gen(), media_type="text/plain")
API Reference
Melissa main methods
| Method | Description |
|---|---|
Melissa.from_config(path) |
Classmethod: load a YAML/TOML/JSON config and register sections |
register(section_id, ...) |
Register a section with examples, template, required_keys |
cache_data(section_id, data,...) |
Cache data for a section (validates required_keys) |
get_cached_data(section_id, ...) |
Read cached data |
invalidate(section_id, ...) |
Delete cached data |
explain(section_id, ...) |
Sync explanation |
aexplain(section_id, ...) |
Async explanation |
explain_stream(section_id, ...) |
Sync streaming generator |
aexplain_stream(section_id, ...) |
Async streaming generator |
list_sections() |
List registered sections |
Melissa.register_cache_backend(name, cls) |
Static: register a third-party cache backend |
Melissa.register_provider(name, dotted_path) |
Static: register a third-party provider |
explain() return shape
{
"reply": "📊 Diego sold $78,500...",
"section_id": "comisiones",
"section_label": "Comisiones del Periodo",
"provider": "<AnthropicProvider model='claude-sonnet-4-20250514'>",
"cached": True,
"usage": {"input_tokens": 234, "output_tokens": 156},
}
Streaming
Sync streaming (scripts, Django views):
for chunk in melissa.explain_stream("comisiones", context_key="tenant_1"):
if chunk["type"] == "text":
print(chunk["content"], end="", flush=True)
elif chunk["type"] == "done":
print(f"\nTokens used: {chunk['usage']}")
Async streaming (FastAPI, async handlers):
async for chunk in melissa.aexplain_stream("comisiones", context_key="tenant_1"):
if chunk["type"] == "text":
await response.write(chunk["content"])
elif chunk["type"] == "done":
logger.info(f"Tokens: {chunk['usage']}")
Async
# Single response (async)
result = await melissa.aexplain("comisiones", context_key="tenant_1")
# Streaming (async)
async for chunk in melissa.aexplain_stream("comisiones", context_key="tenant_1"):
...
Full method matrix:
sync async
complete explain() aexplain()
stream explain_stream() aexplain_stream()
OpenRouter auto mode
When using OpenRouter, the default model is anthropic/claude-sonnet-4 for determinism — new installations get reproducible costs and responses. If you prefer OpenRouter to pick the best model automatically, opt into auto mode:
assistant:
provider: openrouter
model: openrouter/auto # opt-in: OpenRouter picks the best model
api_key: ${OPENROUTER_API_KEY}
You can also specify any other model explicitly: openai/gpt-4o, google/gemini-2.5-pro, meta-llama/llama-4-maverick, etc.
Error handling and retries
from melissa_sage.providers import ProviderError
try:
result = melissa.explain("comisiones", context_key="tenant_1")
except ProviderError as e:
if e.retryable:
# 429 rate limit or 5xx — already retried 3 times with
# exponential backoff. If you see this, all retries failed.
...
else:
# auth error, bad request — not retried (permanent error)
...
Retries are automatic via tenacity. When a provider returns 429 (rate limit) or 5xx (server error), melissa-sage retries up to 3 times with exponential backoff (1s, 2s, 4s). Permanent errors (401 auth, 400 bad request) are raised immediately without retrying. Streaming calls (explain_stream and aexplain_stream) are NOT retried since a partial stream cannot be resumed.
Design decisions
Stateless by design: Each explain() call is independent — there is no conversation history. This is intentional. melissa-sage is designed to explain data sections on demand, not to be a conversational chatbot. Each call gets the full context it needs from the cache and the few-shot examples. This keeps the API simple and the cache scope clean.
No observability hooks in 0.1: Features like on_before_call / on_after_call hooks for logging, metrics, or tracing are planned for a future release. For now, use the usage field in the explain() response to track token consumption, and standard Python logging (melissa_sage logger) for debugging.
Why "sage"?
The name melissa-sage combines two ideas: Melissa (the assistant identity) and sage (wisdom — one who explains with insight). The -sage suffix also ensures the package name is unique on PyPI and leaves room for future packages in the melissa-* family if needed.
Extending
Custom cache backend:
from melissa_sage import Melissa
from melissa_sage.cache.base import BaseCache
class MyDiskCache(BaseCache):
def get(self, key): ...
def set(self, key, data, ttl=None): ...
def delete(self, key): ...
Melissa.register_cache_backend("disk", MyDiskCache)
Custom provider:
from melissa_sage.providers import BaseProvider, ProviderError
class MyProvider(BaseProvider):
def complete(self, system_prompt, messages, max_tokens=1024): ...
async def acomplete(self, system_prompt, messages, max_tokens=1024): ...
def stream(self, system_prompt, messages, max_tokens=1024): ...
async def astream(self, system_prompt, messages, max_tokens=1024): ...
# Register from a dotted path:
Melissa.register_provider("my_provider", "my_pkg.providers.MyProvider")
⚠️ Data privacy notice
This library sends data to external LLM APIs (Anthropic, OpenAI, OpenRouter) unless you explicitly use a self-hosted provider (custom). If your data contains personally identifiable information (PII), make sure you comply with applicable regulations (GDPR, LFPDPPP in Mexico, LGPD in Brazil, etc.). Consider:
- Anonymizing data before calling
cache_data() - Using a self-hosted provider (Ollama, vLLM) for sensitive workloads
- Reviewing your LLM provider's data-retention and training policies
melissa-sage is a tool; compliance is your responsibility.
Roadmap
Features planned for future releases:
- Observability hooks (
on_before_call,on_after_call) for metrics and tracing - Template versioning to detect cache/template mismatches
- Optional conversation history for multi-turn follow-ups
- Configurable retry parameters from YAML
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file melissa_sage-0.1.0.tar.gz.
File metadata
- Download URL: melissa_sage-0.1.0.tar.gz
- Upload date:
- Size: 42.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f329f0bd41b33484ce81434f446fcf80c72effb1c17f620a6a041b0c7a6da179
|
|
| MD5 |
4a1d3c1a8978fc1579af17f0ecc9a3a1
|
|
| BLAKE2b-256 |
f5c29ebafc19d5c18b7b62de5abf06862f57c572b7db1eccbb32cb3c5b8b20c7
|
File details
Details for the file melissa_sage-0.1.0-py3-none-any.whl.
File metadata
- Download URL: melissa_sage-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cecd577ed89440c3e26f48d6a324f29b56accdb2be7b98f0b49cdfdfece112a7
|
|
| MD5 |
3c933f29ca2d82746440c406829a2ab0
|
|
| BLAKE2b-256 |
03cba6c7cea8203f4972fffbc8425bd19089f7cdba93fad331d2c1df842bae3b
|