Skip to main content

Geniffy for Python

CI PyPI License: MIT Docs

Give your app a memory. Write what each of your users tells you, and put what is known about them in front of your model, with where every line came from. When nothing is known, it says so instead of guessing.

pip install geniffy

Make a key in the Geniffy app under API keys and set it as GENIFFY_API_KEY.

from geniffy import Geniffy

client = Geniffy()                       # reads GENIFFY_API_KEY
mem = client.space("customer_1042")      # one of your users; nothing else can read it

source = mem.memories.add("Priya Nair signs the Lumen renewal, and it comes up in March.")
mem.sources.wait(source.id)              # learning usually takes a few seconds

prompt = f"{mem.context('Who signs the Lumen renewal?')}\n\nUser: Who signs the Lumen renewal?"

context() returns the memories that bear on the question, one per line, each with where it came from:

- Priya Nair signs the Lumen renewal.  [note, 2026-10-05]
- The Lumen renewal comes up in March 2027.  [note, 2026-10-05]

Ask about something that was never stored and it returns one sentence, There is nothing stored about this yet. Say so rather than guessing., never an empty string: a model reads silence as permission to invent.

Recall: fast reads every turn, a checked answer when it matters

mem.briefing(cue=user_message)           # what this moment needs, for your system prompt; read, not generated:
                                         # a fraction of a second, fit for every turn of a chat or a call
mem.search("renewal", limit=5)           # the memories, ranked, fast
mem.search_full("port 6543")             # the same, with the lessons, stories and open promises that bear on it
mem.ask("Who signs the renewal?")        # our answer in words, or answer=None and a message saying why
mem.context("Who signs the renewal?")    # the memories an answer stands on, as a block for your own prompt

briefing() and search() read: nothing is generated, so they are fast enough for every turn, voice calls included. ask() and context() have a model judge whether anything bears on the question, which takes a couple of seconds and never invents: use them when an answer must be checked, not on every turn of a call. search() ranks and does not judge: it returns its best matches for any question at all.

Agents: open with the briefing, save every turn

An agent, or any chat that runs over many turns, does two things: it opens each session with the briefing, and it saves every turn as it goes.

system = mem.briefing(project="checkout", cue=task)   # where things stand, what is due, the rules that apply,
                                                      # what happened, then the memories, each dated
run = mem.session(run_id, title="Refund agent")
...
run.save(messages)                                    # after every turn: only what is new is sent

briefing() is written to go straight into a system prompt. briefing_full() has its parts, and each part can be read on its own: now() (where each project stands), episodes(), lessons() and intentions(); set_intention(id, "done") marks a promise kept, and memory_health() says how well the memory answers about its own work.

A session's turns go into one memory for the whole session, tool calls and what came back included, in the shapes OpenAI, Anthropic, the Vercel AI SDK, Gemini and LangChain hold them. save() sends only the messages after the last one it sent, 500 at a time, so a session of any length is never sent twice; if your agent rewrites its history, what it holds now is saved again rather than lost. Sending the new turns yourself instead: mem.memories.add(messages=new_turns, session=run_id).

Voice agents: Pipecat

pip install "geniffy[pipecat]" (Python 3.11 or later), then put GeniffyMemory between the user aggregator and the LLM:

from geniffy import AsyncGeniffy
from geniffy.pipecat import GeniffyMemory

geniffy = AsyncGeniffy()
memory = GeniffyMemory(geniffy.space(f"user_{caller_id}"))
# your system prompt stays on the LLM service: settings=OpenAILLMService.Settings(system_instruction=...)
pipeline = Pipeline([transport.input(), stt, context_aggregator.user(), memory, llm, tts,
                     transport.output(), context_aggregator.assistant()])

Before each reply it puts the caller's briefing, cued by what they just said, in a message of its own just before those words, so Anthropic, Gemini and OpenAI services all send it, whatever sets their system instruction. The first reply waits up to 1.5 seconds for the briefing; a later one waits up to 0.3 seconds for a fresh one, then goes on with the last (timeout, turn_timeout). The call is saved as it goes, so the next call knows it, and a promise moved from Friday to Monday is Monday.

Spaces: one memory per user

A space is your own name for one of your users. Bind a client to it and every call stays inside it. A space exists from the first time you write to it; there is nothing to create.

mem = client.space(f"user_{user.id}")    # per request
client.memories.add("...")               # no space: your own memory, the one the Geniffy app shows
client.spaces()                          # which spaces hold anything, most recently written first
client.space("user_8841").export()      # everything held for that user, as their own copy
client.forget_space("user_8841")         # everything held for that user, gone, when they ask

To let a user's own app or device reach their memory, and nothing else, give it a key limited to them:

key = client.space(f"user_{user.id}").keys.create(name="Asha's phone")   # key.key is shown once
client.space(f"user_{user.id}").keys.revoke(key.id)

Group your users' profiles your way with sections: client.sections.create("billing", keywords=["invoice"]) for every user, or on client.space(id) for one.

Add

mem.memories.add("A note to remember", title="Call with Priya")
mem.memories.add(url="https://example.com")              # a web page, read once
mem.memories.add(messages=chat_history)                        # a conversation, as your framework holds it
mem.memories.add("We moved the launch to March.", said_at="2026-09-12")   # said in the past: dated by when
mem.memories.add_file("Pricing.pdf")                           # PDF, .docx, .pptx, .xlsx or text: a path, bytes or a file opened "rb"
mem.memories.add_many([{"text": "..."}, {"url": "https://..."}])

A file or page that can't be read raises UnreadableError; error.source is the row it left, with the reason.

Syncing your own records? Give each its id. Sent again under the same external_id, the source is updated rather than added twice: only what changed is learned, and what was removed is taken back.

mem.memories.add(ticket.body, title=ticket.subject, external_id=f"ticket-{ticket.id}")
mem.sources.get(external_id=f"ticket-{ticket.id}")
mem.sources.delete(external_id=f"ticket-{ticket.id}")     # when the ticket is deleted in your app

Label what you add with your own name/value pairs, then keep any read to them. Every name must match, and a list of values is any one of them.

mem.memories.add(email.body, title=email.subject, labels={"channel": "email", "account": "lumen"})
mem.context("When does the renewal come up?", labels={"account": "lumen"})
mem.search("pricing", labels={"channel": ["email", "chat"]})
mem.memories.list(labels={"account": "lumen"})
mem.sources.delete_labelled({"channel": "email"})              # the user disconnected it: all it brought goes
mem.sources.delete_labelled({"channel": "email"}, keep=seen)   # the end of a full sync: all but what is still there

To keep a whole data source in step, such as a user's Gmail, Drive or Notion, see Sync a data source.

Read and correct

page = mem.memories.list(kind="people")  # all, people, plan, pref or detail; newest first
for m in mem.memories.iter():            # every memory, a page at a time
    ...
detail = mem.memories.get(42)            # .memory.quote is the sentence it came from; .history its older values
mem.memories.correct(42, "Priya signs it, with Arjun co-signing.")
mem.memories.delete(42)                  # forget it for good
mem.profile("Priya Nair")                # what is lastingly true about someone, and what is going on now
mem.brief("Priya Nair")                  # what to read before talking to them
mem.sources.list(); mem.sources.delete(source.id)   # a source, and what only it taught

Files, kept exactly

Some things have to come back exactly as they were written, such as the notes an agent keeps for itself. A file is held under a path, character for character, and Geniffy also learns from it like a note, so context() and ask() recall what it says. Replacing a file learns only what changed; deleting one takes back what only it taught.

mem.files.put("/notes/lumen.md", "Priya Nair signs the Lumen renewal.\n")   # creates or replaces
mem.files.get("/notes/lumen.md").text                # exactly what was put
mem.files.list("/notes/")                            # paths, sizes and times, by path
mem.files.move("/notes", "/archive/notes")           # a file, or every file in a folder
mem.files.delete("/archive/notes/lumen.md")
mem.files.delete_prefix("/archive/")                 # every file under it

Claude's memory tool

Claude's memory tool keeps notes as files under /memories. GeniffyMemoryTool keeps them in a Geniffy memory: each comes back exactly as Claude wrote it, and what it says is learned too, so context() and ask() recall it anywhere else in your app.

pip install "geniffy[claude]"
import anthropic
from geniffy import Geniffy
from geniffy.claude import GeniffyMemoryTool

claude = anthropic.Anthropic()
memory = GeniffyMemoryTool(Geniffy().space("customer_1042"))   # one of your users: their Claude, their notes

runner = claude.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=16000,
    tools=[memory],
    messages=[{"role": "user", "content": "Remember that I prefer email follow-ups."}],
)
print(runner.until_done().content)

Claude's files are that user's files under /memories, labelled {"channel": "claude-memory"}: mem.files.list("/memories/") lists them, mem.context(question, labels={"channel": "claude-memory"}) recalls only what Claude wrote, and memory.clear_all_memory() deletes them all. A path that could lead out of /memories is refused, typed or URL-encoded. With AsyncAnthropic, use AsyncGeniffyMemoryTool(AsyncGeniffy().space(...)).

Async

from geniffy import AsyncGeniffy

async with AsyncGeniffy() as client:
    mem = client.space("customer_1042")
    await mem.memories.add("Priya wants a demo on Tuesday.")
    print(await mem.context("When is Priya's demo?"))

This month's use

client.usage()   # tokens learned and waiting, answers, what the plan includes, extra learning, when it resets

Saving, briefings, search and recall are free. Past your plan, a source comes back with status "waiting": kept, found by search, and learned next month or at once with extra learning on. Only at the fair-use limit does adding raise UsageLimitError.

Errors, retries and request ids

Every error carries the API's own sentence: AuthenticationError (a wrong or revoked key), NotFoundError, BadRequestError, UnreadableError, RateLimitError, UsageLimitError (this month's use is up; not retried), InternalServerError, APIConnectionError. Reads are retried twice on network errors, 408, 429 and 5xx, and so is putting a file, since the same text put again changes nothing. Adding and moving are retried only when the request never reached Geniffy, or on 429, so a retry never saves a note twice. Set max_retries= and timeout= on the client to change that.

Every response carries an X-Request-ID, and every error carries it as error.request_id. Paste it into Requests in the Geniffy app to see that exact call: what was asked, what came back, and how long it took.

A key reaches its owner's memory and every space beneath it. Keep it on your server.

Docs: https://docs.geniffy.com

Metadata

Release files for geniffy 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for geniffy 0.5.0
File Size Uploaded
geniffy-0.5.0.tar.gz 66.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for geniffy 0.5.0
File Interpreter ABI Platform
geniffy-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 104.0 kB

Release files / geniffy-0.5.0.tar.gz

Download URL geniffy-0.5.0.tar.gz
Size 66.3 kB
Tags Source
SHA-256 checksum
How to use checksums
97c63a2f5410cb4c73d5f4c87cfb456e1b1d6a100b3ffa49d25c1d488f6dd1cc
BLAKE2b-256 checksum
How to use checksums
1a6adcfce03ab330e637be5cb680932140a8d19ee439e1e8a6748d1fd9f68a1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / geniffy-0.5.0-py3-none-any.whl

Download URL geniffy-0.5.0-py3-none-any.whl
Size 37.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
72124d9b04cd33229083a255f6407a87931f1f3f69001fc9e56a247326b5088a
BLAKE2b-256 checksum
How to use checksums
706c3058ced8c6fe076f923c14cdd90dfbdd2f8b2cdfa372180055ca3b19c585
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page