Skip to main content
Memory stores text and documents and returns the relevant chunks at query time. Use it to ground responses in your own data, give agents long-term context, or run it as a managed semantic datastore. A typical use: drop a folder of support tickets into a knowledge base and pull the closest past resolutions into every new reply. Unlike the other Control Plane features, Memory has no rules to configure. You call it from code and manage knowledge bases visually in the Memory section of platform.opper.ai. Each knowledge base is one isolated store of entries and uploaded files.

How it works

When you add data, Memory chunks the content into segments and embeds each chunk with azure/text-embedding-3-large. At query time it embeds the query with the same model, retrieves the most similar chunks, and reranks them so the most relevant come first.

Adding text entries

Use add for structured records (tickets, notes, FAQ entries) that you can serialise to a string. Each call writes one entry. Re-adding with the same key overwrites the previous entry, so use it for idempotent writes.
from opperai import Opper
from pydantic import BaseModel
from typing import Literal
import os

opper = Opper(api_key=os.getenv("OPPER_API_KEY"))

class SupportTicket(BaseModel):
    ticket_id: str
    issue_description: str
    issue_resolution: str
    status: Literal['open', 'in_progress', 'resolved', 'closed']

try:
    kb = opper.knowledge.get_by_name(name="Tickets")
except Exception:
    kb = opper.knowledge.create(name="Tickets")

ticket = SupportTicket(
    ticket_id="123",
    issue_description="I can't log in. Password reset emails never arrive.",
    issue_resolution="Verified the user is on the new auth provider and re-triggered the welcome email.",
    status="resolved",
)

opper.knowledge.add(
    kb.id,
    key=ticket.ticket_id,  # unique key; re-adding with the same key overwrites
    content=ticket.model_dump_json(),
    metadata={"source": "our_ticket_system", "status": ticket.status},
)

Uploading files

upload ingests files end-to-end. Memory extracts text from PDFs, Word documents, plain text, and Markdown, then chunks and embeds them in the background.
cURL
# Upload a file (multipart)
curl -X POST https://api.opper.ai/v2/knowledge/{KNOWLEDGE_BASE_ID}/upload \
  -H "Authorization: Bearer ${OPPER_API_KEY}" \
  -F "file=@./handbook.pdf"
The response carries the file’s id, storage key, original_filename, and a numeric document_id. List the knowledge base’s files to watch indexing progress:
cURL
curl https://api.opper.ai/v2/knowledge/{KNOWLEDGE_BASE_ID}/files \
  -H "Authorization: Bearer ${OPPER_API_KEY}"
{
  "meta": {"total_count": 1},
  "data": [
    {
      "id": "2fc2fa70-5fd1-499f-8920-ccaa5c38e50f",
      "original_filename": "handbook.pdf",
      "size": 184230,
      "status": "indexing",
      "document_id": 469869,
      "metadata": {}
    }
  ]
}
Once status flips to indexed, the file’s chunks join the same query index as your add entries.

Querying

Querying is the core read path. Pass a natural-language string and Memory returns the chunks closest in meaning, ranked by relevance.
results = opper.knowledge.query(kb.id, query="Can't login", top_k=3)
for r in results:
    print(round(r.score, 3), r.content[:80])
Query returns a flat list of chunks. Each one carries its parent entry’s metadata and a score:
[
  {
    "id": "c3f6d2f6-c543-456d-be63-bed491c549fa",
    "key": "c3f6d2f6-c543-456d-be63-bed491c549fa",
    "content": "I can't log in. Password reset emails never arrive…",
    "metadata": { "source": "our_ticket_system", "status": "resolved" },
    "score": 0.44
  }
]
The key you supply to add lives at the entry level, but query returns one row per chunk. The id and key on each result are chunk identifiers, not your entry key. Put your own identifier in metadata (e.g. ticket_id) if you need it back.

Filters

Narrow a query to chunks whose parent-entry metadata matches. Each filter is a {field, operation, value} triple; all filters AND together. A query with no matches returns an empty array.
tickets = opper.knowledge.query(
    kb.id,
    query="Can't login",
    top_k=3,
    filters=[
        {"field": "status", "operation": "=", "value": "resolved"},
        {"field": "source", "operation": "=", "value": "our_ticket_system"},
    ],
)

Use with the JSON API

Query results plug straight into a call as context. This is the standard RAG flow: retrieve relevant chunks, then ground a structured generation on them.
class SuggestResolution(BaseModel):
    thoughts: str
    message: str
    reference_ticket_ids: list[int]

completion = opper.call(
    "suggest_resolution",
    instructions="Given a user question and a list of potentially relevant past tickets, provide a suggestion for a resolution to the support agent",
    input={"past_tickets": tickets, "user_issue": "Can't login"},
    output_schema=SuggestResolution,
)

print(completion.data)
You’ll usually want to pull only the relevant fields out of each result before passing them as context. Chunk-level id/key fields are rarely useful to the model and burn tokens.

API surface

MethodPathPurpose
GET/v2/knowledgeList knowledge bases.
POST/v2/knowledgeCreate a knowledge base.
GET/v2/knowledge/{id}Fetch a knowledge base (includes entry count).
GET/v2/knowledge/by-name/{name}Look up by name.
DELETE/v2/knowledge/{id}Delete the base and its contents.
POST/v2/knowledge/{id}/addAdd a text entry with key, content, metadata.
POST/v2/knowledge/{id}/uploadUpload a file (multipart).
GET/v2/knowledge/{id}/filesList uploaded files with indexing status.
POST/v2/knowledge/{id}/queryHybrid query; supports top_k and filters.

Inspecting knowledge bases

The Memory section in the platform lists every knowledge base in the project. Create, update, and delete bases there, and inspect what’s been indexed. List of indexes