Skip to content

Run on Pixeltable 0.7.6, add Memory.search(), and add CI - #3

Merged
pierrebrunelle merged 3 commits into
mainfrom
fix/pixeltable-0.7.6
Sep 10, 2026
Merged

pierrebrunelle merged 3 commits into
mainfrom
fix/pixeltable-0.7.6

Conversation

@pierrebrunelle

Copy link
Copy Markdown
Member

Why

Document, audio and video memories raised on construction against any Pixeltable >= 0.5.6. config.py carried the 0.4.2 iterator parameter names, and those names are not cosmetic — they go through dataclasses.asdict() straight into the iterator call, so each field name is an API contract:

pixelmemory passed 0.7.6 expects
chunk_duration_sec duration
overlap_sec overlap
min_chunk_duration_sec min_segment_duration
html_skip_tags skip_tags

The pixeltable.iterators shims forward **kwargs verbatim with no name mapping, which is why these were hard errors rather than deprecation warnings.

Separately, every retrieval example was dead code: they all opened with memory.chunk_views["..."], and chunk_views is a list of dataclasses on resources, not a mapping on Memory.

This went unnoticed because there is no CI on any branch and uv.lock pinned pixeltable 0.4.2. The most recent commit on main is 766c5ba "Removed test".

What is here

1. Green baseline + CI (d5091a3)

Iterator parameter renames and the deprecated shims swapped for document_splitter/audio_splitter/string_splitter/frame_iterator. Plus:

  • the audio view output column is audio_segment, not audio_chunk
  • transcriptions() kwargs were splatted top-level; the signature is (audio, model, model_kwargs), so WhisperParams(language='en') raised
  • if_exists="replace_force" on five paths dropped and rebuilt every view and index on every construction, re-running Whisper and re-embedding everything
  • non-nullable-by-default (0.7.3) made every field mandatory; Context gains required
  • pxt.list_dirs() returns slash paths, so the dotted-namespace check never matched and nested namespaces failed on the second construction
  • __getattr__ dereferenced self.resources from inside itself, so any __init__ failure became RecursionError
  • vision.py off deprecated openai.vision; the anthropic path gains its required max_tokens and base64-encodes the image instead of passing a raw column reference as the data value

The suite is construction-only: no API key, no model weights, no spaCy, no media fixtures. conftest.py pops the provider keys from the environment so the tests prove it. It asserts on get_metadata()['iterator_call'], which pins the kwarg-rename class of bug precisely, and on view ids being stable across reconstruction, which pins replace_force. CI installs pixeltable unpinned with a weekly cron. Against the pre-fix source it reports 14 failures.

2. Memory.search() and Memory.views() (494ca94)

MemoryResources gains a search_targets list populated at each of the seven places an embedding index is created. Video registers its frames under "<col>_frames" so transcript and visuals can be searched separately.

memory.search("feline animal", limit=2)
memory.search(query, on="report")

search() always passes idx=, because chunked text carries two indexes on one column and a bare .similarity() there is ambiguous.

3. Repair the six broken examples (4ebcb9e)

Most collapse to one line. Two keep a raw handle deliberately and say why in a comment: the langchain example filters on metadata as well as ranking, and images.py returns the image alongside its caption. Two grep tests keep positional .similarity() and chunk_views[...] from coming back.

Public API

Memory(context=, namespace=, table_name=, if_exists=), memory.Entry/add(), the __getattr__ passthrough, and all Context field names are unchanged.

Deliberate breaks, both needing a 0.2.0 bump: the two splitter param dataclasses are renamed (unavoidable — those names are the iterator kwargs), and views/indexes now survive reconstruction instead of being rebuilt.

Still to do

Partial; landing it reviewable rather than sitting on it. Not yet done: uuid7() primary keys + Context(index=), as_tools()/lookup_tool(), Memory.router() replacing the 280-line FastAPI example, and examples/app.py.

A question worth your judgement

The audit raised whether the wrapper still earns its keep, and I think it partly does not. FastAPIRouter is verified working against runtime-built tables (add/search/lookup/delete all returned real rows via TestClient), which makes most of examples/fastapi/memory_service.py deletable. What survives is the one thing TableModel cannot express: a schema chosen at runtime from Memory(context=json.load(f)), plus the multimodal ingest pipelines. A dynamic TableModel is possible — I verified it — but it leans on sys._getframe(1) and a table-name-keyed registry, so I did not take it.

🤖 Generated with Claude Code

pierrebrunelle and others added 3 commits September 9, 2026 21:23
Document, audio and video memories raised on construction against any
Pixeltable >= 0.5.6. config.py carried the 0.4.2 iterator parameter names,
and those names are not cosmetic: they go through dataclasses.asdict()
straight into the iterator call, so each one is an API contract.
chunk_duration_sec/overlap_sec/min_chunk_duration_sec are now
duration/overlap/min_segment_duration, and html_skip_tags is skip_tags.
The pixeltable.iterators shims forward **kwargs verbatim with no name
mapping, which is why these were hard errors rather than warnings; they are
replaced by document_splitter / audio_splitter / string_splitter /
frame_iterator.

Also fixed:
- the audio view output column is `audio_segment`, not `audio_chunk`
- transcriptions() kwargs were splatted top-level; the signature is
  (audio, model, model_kwargs), so WhisperParams(language='en') raised
- if_exists="replace_force" on five paths dropped and rebuilt every view and
  index on every construction, re-running Whisper and re-embedding
  everything; now "ignore", which the existing get_table fallbacks expect
- non-nullable-by-default (0.7.3) made every field mandatory; Context gains
  `required` so a field can be optional
- pxt.list_dirs() returns slash paths, so the dotted-namespace check never
  matched and nested namespaces failed on second construction; replaced by
  create_dir(parents=True, if_exists="ignore")
- __getattr__ dereferenced self.resources from inside itself, so any __init__
  failure turned the next attribute access into RecursionError
- vision.py moves off the deprecated openai.vision to chat_completions, and
  the anthropic path gains its required max_tokens, base64-encodes the image
  instead of passing a raw column reference as the data value, and routes
  llm_kwargs through model_kwargs

None of this was caught because there was no CI on any branch and uv.lock
pinned pixeltable 0.4.2, so local installs resolved to a version where the
code worked while `pip install pixelmemory` got a package that could not run
its own README. The most recent commit on main was "Removed test".

The new suite is construction-only, so it needs no API key, no model weights,
no spaCy and no media fixtures; conftest.py pops the provider keys from the
environment so the tests prove it. It asserts on get_metadata()['iterator_call']
(which catches the kwarg-rename class of bug precisely) and on view ids being
stable across reconstruction (which catches replace_force). CI installs
pixeltable unpinned with a weekly cron, so the next breaking release turns
this build red instead of a user's install.

Against the pre-fix source the suite reports 14 failures; against this commit,
19 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pixelmemory indexed your data and then left retrieval to you. Every caller
wrote the same block by hand:

    chunk_view = memory.chunk_views["report"]
    sim = chunk_view.text.similarity(query)
    results = (chunk_view.order_by(sim, asc=False).limit(3)
               .select(chunk_view.text, similarity=sim).collect())

That appeared eight times across the examples and README, and it did not
work: `chunk_views` is a list of dataclasses on `resources`, not a mapping on
`Memory`, so the first line raised AttributeError. It also used the
positional `.similarity(query)` form, deprecated since 0.5.7.

MemoryResources gains a `search_targets` list, populated at each of the seven
places an embedding index is created: the document chunk view, the audio
sentence view, chunked and unchunked strings, the direct string index, image
descriptions, and video frame captions. Video registers its frames under
"<col>_frames" so a caller can search transcript and visuals separately.

    memory.search("feline animal", limit=2)
    memory.search(query, on="report")

search() always passes idx=, because chunked text carries two indexes on one
column and a bare .similarity() there is ambiguous. views() returns the raw
table handles for queries search() does not cover, which is what the broken
`memory.chunk_views["report"]` was reaching for.

Errors name what is available rather than failing blankly: searching an
unknown context id lists the real ones, and searching a memory with no
embedding index says to pass embed=True.

Suite goes 19 -> 26. Most are metadata-only; two rank real embeddings, which
downloads a small sentence-transformers model but still needs no API key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every multimodal and integration example opened its retrieval block with
`memory.chunk_views["..."]`, which raised AttributeError: chunk_views is a
list of dataclasses on `resources`, not a mapping on Memory. So audio.py,
pdf.py, website.py, video.py, crewai_agentic_rag.py and
langchain_financial_rag_agent.py all failed at the search step.

Most collapse to one line now:

    results = memory.search(query, on="audio", limit=3)

video.py searches transcript and frame captions separately, which is what the
"<col>_frames" registration is for. text.py's `where(sim >= 0.1)` becomes
search(min_score=0.1).

Two keep a raw handle deliberately, because search() does not cover what they
do, and both say so in a comment: langchain_financial_rag_agent.py filters on
metadata as well as ranking, and images.py returns the image alongside its
caption. Both move to views() and the keyword `.similarity(string=..., idx=...)`.

The remaining positional `.similarity(query)` calls are gone, in the examples,
in the README, and in the FastAPI service. Two grep tests keep them gone: one
for positional similarity, one for chunk_views/frame_views access. They are
greps rather than execution tests because most examples need media files or a
provider key to run.

Suite 26 -> 28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pierrebrunelle
pierrebrunelle merged commit 20448d5 into main Sep 10, 2026
4 checks passed
pierrebrunelle added a commit that referenced this pull request Sep 10, 2026
* chore: document the migration, scope the library, bump to 0.2.0

PyPI still serves 0.1.0 with an unbounded `pixeltable>=0.3.8`, so a fresh
`pip install pixelmemory` resolves Pixeltable 0.7.6 and gets a package whose
Document, Audio and Video memories raise on construction. #3 fixed the repo;
this is what makes the fix reachable.

Adds a "Migrating from 0.1.x" section covering the three caller-visible
changes: Python 3.11+, the two renamed splitter parameter dataclasses, and
search()/views() replacing the chunk_views/frame_views subscripts. The
renames are called out as unavoidable, because those field names are passed
straight through to Pixeltable's iterators and so are the API rather than a
naming choice.

Adds "When not to use pixelmemory". If the schema is fixed when you write the
code, TableModel and FastAPIRouter do this natively and the wrapper is not
earning anything; it is worth saying so rather than letting someone discover
it later. What pixelmemory is actually for is a schema chosen at runtime, plus
the multimodal ingest pipelines it wires in the right dependency order.

The two grep gates now skip the migration section: a migration guide has to
quote the old broken calls to show what replaces them, so scanning it for
those calls failed. Verified the gates still fire on a violation injected
outside that section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: regenerate uv.lock for 0.2.0, and make CI check it

The version bump left uv.lock recording 0.1.0. CI did not notice, because it
ran a bare `uv sync`, which quietly rewrites the lock instead of failing.
pixelagent's workflow uses --locked and caught the same drift immediately, so
this adopts it: a lockfile that disagrees with pyproject.toml is now a red
build rather than a silent fixup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant