Skip to main content

AI contextual assistant with few-shot learning and context injection. Provider-agnostic, framework-agnostic.

Project description

melissa-sage 🌿

PyPI version Python versions License: MIT

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


Download files

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

Source Distribution

melissa_sage-0.1.0.tar.gz (42.8 kB view details)

Uploaded Source

Built Distribution

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

melissa_sage-0.1.0-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

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

Hashes for melissa_sage-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f329f0bd41b33484ce81434f446fcf80c72effb1c17f620a6a041b0c7a6da179
MD5 4a1d3c1a8978fc1579af17f0ecc9a3a1
BLAKE2b-256 f5c29ebafc19d5c18b7b62de5abf06862f57c572b7db1eccbb32cb3c5b8b20c7

See more details on using hashes here.

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

Hashes for melissa_sage-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cecd577ed89440c3e26f48d6a324f29b56accdb2be7b98f0b49cdfdfece112a7
MD5 3c933f29ca2d82746440c406829a2ab0
BLAKE2b-256 03cba6c7cea8203f4972fffbc8425bd19089f7cdba93fad331d2c1df842bae3b

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