Metadata-Version: 2.5
Name: recall-livekit
Version: 0.2.0
Summary: Typed, correctable long-term memory for LiveKit voice agents, backed by Recall by Polign
Project-URL: Homepage, https://polign.com
Project-URL: Documentation, https://polign.com/integrations.html
Project-URL: Repository, https://github.com/Polign/polign
Project-URL: Issues, https://github.com/Polign/polign/issues
Author: Polign
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agent memory,livekit,polign,recall,voice agent
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: livekit-agents<2,>=1.8
Requires-Dist: polign-recall>=0.3.0
Description-Content-Type: text/markdown

# recall-livekit

Long-term memory for [LiveKit Agents](https://github.com/livekit/agents) voice
agents, backed by [Recall by Polign](https://polign.com/recall.html).

The agent loads everything it knows about the caller before the first reply,
and the model saves new facts through a `remember` tool. Facts are typed:
each one is a predicate from a closed registry with a value, so "call me Sam"
replaces the old name instead of piling up a second one, and the history of
the change is kept. No second model runs on the read path, and no embedding
service is needed.

```bash
pip install recall-livekit "livekit-agents[openai,deepgram,cartesia,silero]"
```

That is the whole install. pip also brings the
[`polign_db`](https://pypi.org/project/polign-db/) package with the `polign`
CLI and `polign-server` binaries for Linux, macOS and Windows, so there is no
separate database to download, and a worker image needs nothing beyond
`pip install`.

## Usage

```python
from livekit.agents import AgentServer, AgentSession, JobContext, JobProcess, cli
from livekit.plugins import cartesia, deepgram, openai, silero
from recall_livekit import VOICE_REGISTRY, RecallAgent, RecallMemory

server = AgentServer()


def setup(proc: JobProcess) -> None:
    # one Recall subprocess per worker process
    proc.userdata["recall"] = RecallMemory.open(
        local_dir="./recall-data",            # the database lives here
        predicates=VOICE_REGISTRY,            # or your own registry file
    )


server.setup_fnc = setup


@server.rtc_session()
async def entrypoint(ctx: JobContext) -> None:
    participant = await ctx.wait_for_participant()
    memory = ctx.proc.userdata["recall"].for_subject(participant.identity)

    session = AgentSession(
        stt=deepgram.STT(), llm=openai.LLM(model="gpt-4.1-mini"),
        tts=cartesia.TTS(), vad=silero.VAD.load(),
    )
    await session.start(
        agent=RecallAgent(
            memory=memory,
            instructions="You are the support line for Acme. Use what you remember about the caller.",
        ),
        room=ctx.room,
    )


if __name__ == "__main__":
    cli.run_app(server)
```

What happens on a call:

1. `on_enter` loads the caller's current beliefs and appends them to the
   instructions inside a `<recall_memory>` block, before the greeting.
2. The caller says "actually call me Sam, and I moved to Denver". The model
   calls `remember` twice. Recall supersedes the old name and timezone, and
   the agent rewrites its instructions so the next sentence already uses Sam.
3. A week later the same identity calls back and step 1 finds the facts.

Nothing is searched per turn. A caller's typed facts are a short list, so the
whole set fits in the prompt. When a caller has more beliefs than `limit`
(default 20), each user turn also runs a search and adds matching facts to
that turn only.

## Where the memory is stored

`local_dir="./recall-data"` keeps the database on the worker's machine. The
first worker process to open the directory starts a `polign-server` for it in
the background, listening on localhost only, and every other process shares
that server. It keeps running between worker restarts; its process id is in
`recall-data/runtime.json` and its log in `recall-data/server.log`. This is
the quickest way to try the package and is fine for a single machine. It works
on Linux and macOS.

Workers on several machines need one shared server. Run `polign-server`
somewhere they can all reach ([Get started](https://polign.com/developers.html)
shows how, including storing into S3, GCS or Azure), then connect to it
instead:

```python
RecallMemory.open(
    url="http://memory.internal:23000",   # or POLIGN_URL in the environment
    api_key=os.environ["POLIGN_API_KEY"],
    predicates=VOICE_REGISTRY,
)
```

## Your own Agent subclass

```python
from recall_livekit import attach

agent = FrontDesk()                      # any Agent with string instructions
await attach(agent, memory, who="the guest")
await session.start(agent, room=ctx.room)
```

`attach` adds the memory block and the `remember` tool. It does not do the
per-turn search for overflowing callers; use `RecallAgent` for that.

## Options

| Where | Option | Default | What it does |
|---|---|---|---|
| `RecallMemory.open` | `local_dir` | none | Keep the database in this directory and run its server; exclusive with `url` and `api_key` |
| `RecallMemory.open` | `url`, `api_key`, `collection`, `predicates` | worker environment | Connection for the subprocess (`POLIGN_URL`, `POLIGN_API_KEY`, `POLIGN_COLLECTION`, `POLIGN_PREDICATES`) |
| `RecallMemory.open` | `command` | `polign mcp -memory-only -write` | The subprocess argv, to run a `polign` binary other than the one pip installed |
| `for_subject` | `limit` | 20 | Beliefs loaded into the prompt; above it, per-turn search kicks in |
| `for_subject` | `read_timeout`, `write_timeout` | 0.5 s, 5 s | Reads fail open (last known beliefs); writes tell the model the fact was not saved |
| `RecallAgent` | `context_template` | `<recall_memory>\n{context}\n</recall_memory>` | Wrapper around the block; must contain `{context}` |
| `RecallAgent` | `who` | `"the caller"` | How the block refers to the person |
| `RecallAgent` | `remember_tool`, `forget_tool` | on, off | Which tools the model gets |
| `RecallAgent` | `search_when_overflowed` | on | Per-turn search when the caller has more beliefs than `limit` |

## Custom predicates

Predicates are a JSON file. The `remember` tool's schema is built from it, so
the model only ever sees the names you allow.

```json
{
  "name":       { "cardinality": "single", "value_type": "string",  "description": "The name the caller asks to be called" },
  "open_issue": { "cardinality": "multi",  "value_type": "string",  "description": "A problem the caller reported that is not resolved yet" }
}
```

`single` means a newer value replaces the old one; `multi` means each value
is an additional fact. `value_type` is `string`, `number`, or `boolean`. The
file replaces Recall's built-in registry entirely. `VOICE_REGISTRY` is a
starter set for callers: name, preferred language, callback number, email,
timezone, account tier, recording consent, response style, open issues,
owned products, and technologies used.

## Subjects and retention

Use a stable, auth-derived identifier as the subject, such as the participant
identity your token server issued. Never the room name. Recall keeps the
record of every change while the current view updates; enable `forget_tool`
if callers should be able to withdraw a fact by asking, and see the Recall
docs for retention and export.

## Development

```bash
cd python/recall-livekit
python -m pip install -e . pytest pytest-asyncio
pytest tests/unit_tests -q                # fake Recall subprocess, scripted model
pytest tests/integration_tests -v         # real polign-server and polign CLI
```

The integration tests locate the binaries like the SDK's tests do
(`POLIGN_SERVER`, `POLIGN_SOURCE`, `POLIGN_SERVER_VERSION`, or the latest
release download) and skip when none is found. To test against the binaries
pip installed, set `POLIGN_SERVER="$(python -c 'import polign_db; print(polign_db.find_bin("polign-server"))')"`.
