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]

Pick the install extra that matches your provider:

Provider Install command Env var
Anthropic pip install melissa-sage[anthropic] ANTHROPIC_API_KEY
OpenAI pip install melissa-sage[openai] OPENAI_API_KEY
OpenRouter pip install melissa-sage[openai] OPENROUTER_API_KEY
Custom (Ollama/vLLM/LM Studio) pip install melissa-sage[openai] (optional)
All providers pip install melissa-sage[all]

Note: OpenRouter and Custom both use the [openai] extra because they piggyback on the openai SDK underneath. See the Provider cookbook below for full YAML examples per provider.

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

⚠️ Config gotchas

Three things you'll probably hit when writing your first melissa_sage.yaml. Reading this section will save you a debugging round.

1. The top-level LLM block MUST be named exactly assistant:

Melissa.from_config() looks for the literal key assistant:. Any other name (provider:, llm:, config:, etc.) is silently ignored, and melissa-sage falls back to an Anthropic default with no API key — producing a confusing authentication error at the first LLM call.

Since 0.1.1, from_config() raises ConfigError immediately if assistant: is missing, so this is now a loud failure instead of a silent one:

from melissa_sage import Melissa, ConfigError

try:
    melissa = Melissa.from_config("melissa_sage.yaml")
except ConfigError as e:
    print(e)  # names the missing key and shows an example

2. ${VAR} env-var interpolation runs on the ENTIRE config tree

melissa-sage walks every string value in your loaded config and substitutes ${VAR_NAME} with os.environ["VAR_NAME"]. This is how api_key: ${ANTHROPIC_API_KEY} works — but it also means the substring ${{ inside a Jinja template (e.g. ${{ price }}, intended as a literal $ followed by a Jinja expression) collides with the env-var regex: the regex greedily matches ${...} up to the first }, tries to resolve { price as a variable name, and crashes with EnvironmentError: Environment variable '{ price ' not defined.

You have three clean ways to write a literal ${ in a template:

  1. $${{ price }} — since 0.1.1, a double dollar escapes to a literal ${, and the rest is Jinja. Cleanest.
  2. {{ "$" }}{{ price }} — use a Jinja literal for the dollar sign, then the variable in a separate Jinja block. Works on any version.
  3. {{ price | money }} — if you just want currency formatting, use the built-in money filter, which emits $1,234. No escape needed.

3. template: and examples: accept several forms, not just file paths

Both fields auto-detect their source type. The next section enumerates every working form.

Section definition forms

Every section in the sections: block can define its template: and examples: using any of these forms. melissa-sage auto-detects the type at load time.

template:

Value Behavior
"prompts/foo_template.yaml" Load .yaml/.yml file (reads content: key)
"prompts/foo_template.json" Load .json file (reads content: key)
"Hello {{ name }}" Inline Jinja string (detected by {{)
{content: "Hello {{ name }}"} Inline dict (since 0.1.1)
None (or unset) Fallback: render input dict as indented JSON
"https://example.com/tpl.yaml" Fetch URL as YAML

examples:

Value Behavior
"prompts/foo_examples.yaml" Load file (reads examples: key)
[{user: "hi", assistant: "hello"}] Inline list
None (or unset) No few-shot examples
"https://example.com/examples.json" Fetch URL as JSON

The file-based forms are recommended for more than ~2 examples or templates longer than ~5 lines. Inline forms are fine for tiny configs where keeping everything in one file is nicer than managing a sidecar directory.

Few-shot examples

Few-shot examples teach the model the exact format, tone, and structure you want. They are injected as alternating user/assistant conversation turns before the real user question, so the model has seen the format once before it has to produce one.

External file form

Referenced from your main config as examples: prompts/salarios_examples.yaml. The file's top-level key must be examples::

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

Inline form

Good for 1-2 examples you don't want to split into a sidecar file:

sections:
  salarios:
    label: Salarios
    template: prompts/salarios_template.yaml
    examples:
      - user: "Explain this"
        assistant: "A short explanation here."

Jinja2 templates

Templates take cached data and render it as the context message sent to the model. melissa-sage compiles them with Jinja2 and makes four custom filters available:

Filter Input Output
money 145000 $145,000
money2 145000 $145,000.00
pct 5 5%
num 145000 145,000

External file form

Referenced from your main config as template: prompts/salarios_template.yaml. The file's top-level key must be content::

# prompts/salarios_template.yaml
content: |
  [Context — Salaries — Period: {{ periodo }}]

  {% for e in employees %}
  • {{ e.nombre }}: {{ e.salario | money }} MXN
  {% endfor %}

Inline forms

Good for short templates. Both of these are equivalent:

sections:
  salarios:
    label: Salarios
    # (a) inline Jinja string — auto-detected by the presence of {{
    template: "Total: {{ employees | length }} empleados"

    # (b) inline dict with a `content` key (since 0.1.1) — same behavior
    # template: {content: "Total: {{ employees | length }} empleados"}

Multi-line inline templates work too, thanks to YAML's | block scalar:

sections:
  salarios:
    template: |
      [Context — Salaries]
      {% for e in employees %}
      • {{ e.nombre }}: {{ e.salario | money }}
      {% endfor %}

Providers

Provider YAML key Default model Extra
Anthropic anthropic claude-sonnet-4-20250514 [anthropic]
OpenAI openai gpt-4o [openai]
OpenRouter openrouter anthropic/claude-sonnet-4 [openai]
Custom custom llama3.2 (Ollama by default) [openai]

Switching providers is a single line in the YAML. See the Provider cookbook below for a complete example per provider.

Provider cookbook

All four providers share the same assistant: block — only the provider value, default model, and env var change. Pick the one that fits your deployment.

Anthropic (direct)

assistant:
  provider: anthropic
  model: claude-sonnet-4-20250514
  api_key: ${ANTHROPIC_API_KEY}
  max_tokens: 1024

Install with pip install melissa-sage[anthropic]. Uses the official anthropic SDK.

OpenAI (direct)

assistant:
  provider: openai
  model: gpt-4o
  api_key: ${OPENAI_API_KEY}
  max_tokens: 1024

Install with pip install melissa-sage[openai].

OpenRouter (access Claude / GPT / Gemini / Llama with one API key)

assistant:
  provider: openrouter
  model: anthropic/claude-sonnet-4    # default — override with any OpenRouter model id
  api_key: ${OPENROUTER_API_KEY}
  max_tokens: 1024

Install with pip install melissa-sage[openai]. The OpenRouter provider piggybacks on the openai SDK.

The api_base is auto-set to https://openrouter.ai/api/v1 — you do not need to configure it. Only override if you proxy OpenRouter through a different gateway.

You can swap the model to any OpenRouter-supported id:

    model: openai/gpt-4o
    model: google/gemini-2.5-pro
    model: meta-llama/llama-4-maverick
    model: openrouter/auto          # OpenRouter picks the best model server-side

Self-hosted (Ollama, vLLM, LM Studio, etc.)

assistant:
  provider: custom
  model: llama3.2
  api_base: http://localhost:11434/v1   # Ollama's default
  api_key: not-needed                   # most self-hosted servers don't check
  max_tokens: 1024

Install with pip install melissa-sage[openai]. The default api_base is http://localhost:11434/v1 (Ollama), and the default api_key is the literal string "not-needed" — both are overridable. Point api_base at your vLLM / LM Studio / TGI endpoint if you're running something else.

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()

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
  • Scoped env-var resolution: apply ${VAR} substitution only to specific config keys (api_key, api_base, cache.url, etc.) instead of the whole tree, eliminating the Jinja-template collision described in the Config gotchas section
  • Full inline section definitions: a single unified section: block format that carries template + examples + metadata without needing external files, for configs that fit on one screen
  • melissa-sage validate CLI: a command that loads a config file and reports all issues before the first LLM call

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.1.tar.gz (48.9 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.1-py3-none-any.whl (30.9 kB view details)

Uploaded Python 3

File details

Details for the file melissa_sage-0.1.1.tar.gz.

File metadata

  • Download URL: melissa_sage-0.1.1.tar.gz
  • Upload date:
  • Size: 48.9 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.1.tar.gz
Algorithm Hash digest
SHA256 d36ab804f745039aa16d52c5e58cbd4f16a6774f192e334957f5d776242618f7
MD5 ddac797a3710afb75598e0839957829f
BLAKE2b-256 a2ec4666946103cc417aba6c18ed27f2389cd1258eed072ef58573f0277ad54f

See more details on using hashes here.

File details

Details for the file melissa_sage-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: melissa_sage-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 30.9 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 10bee43c8e9eac42921151a8d3476af41b69a713db5b2c4f33056723ec617116
MD5 cf6da79a3519cd008e058f1d7a640f82
BLAKE2b-256 944929bc291b6e89a9968031b253c15c05e6126b4adcb83ce901ab76f20cf4c4

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