Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: test

# No `secrets:` block appears anywhere in this file, and that absence is the
# assertion: the suite must pass with no provider credentials at all. A test that
# needs a key fails here.

on:
push:
branches: ["**"]
pull_request:
workflow_dispatch:
schedule:
# Weekly canary. The install step below takes Pixeltable unpinned, so the
# release that breaks pixelmemory turns this build red before a user finds it.
- cron: "0 6 * * 1"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Install
run: uv sync --python ${{ matrix.python-version }}

- name: Take Pixeltable unpinned
# Deliberately ignores the `<0.8` pin in pyproject.toml. This is the canary.
# Every step after this uses --no-sync, or uv would restore the pinned version.
run: uv pip install --upgrade pixeltable

- name: Record the Pixeltable version under test
run: uv run --no-sync python -c "import pixeltable; print(pixeltable.__version__)"

- name: Lint
run: uv run --no-sync ruff check .

- name: Test
env:
HF_HUB_DISABLE_PROGRESS_BARS: "1"
run: uv run --no-sync python -m pytest tests/ -v
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
**/__pycache__
*.egg-info
*.py[cod]
.pytest_cache/
.ruff_cache
.ruff_cache/
.venv
.venv/
.vscode
__pycache__/
dist
**/__pycache__
*.egg-info
resources/
.ruff_cache
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ entry = memory.Entry(content="I love Python programming", user_id="user_123")
memory.add(entry)

# Semantic search
similarity = memory.content.similarity("programming languages")
results = memory.where(similarity >= 0.5).collect()
results = memory.search("programming languages", min_score=0.5)
for r in results:
print(r["score"], r["text"])
```

## Architecture
Expand Down Expand Up @@ -111,8 +112,7 @@ entry = memory.Entry(content="Learning about AI")
memory.add(entry)

# Semantic search
sim = memory.content.similarity("artificial intelligence")
results = memory.where(sim >= 0.3).collect()
results = memory.search("artificial intelligence", min_score=0.3)
```

### Multimodal Memory
Expand Down
4 changes: 2 additions & 2 deletions examples/fastapi/memory_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
from pixelmemory import Memory
from pixelmemory.context import Text, Image, Video, Audio, Document
Expand Down Expand Up @@ -160,7 +160,7 @@ def search_items(
# Apply semantic search if query and search_column are provided
if query and search_column:
try:
similarity = getattr(mem, search_column).similarity(query)
similarity = getattr(mem, search_column).similarity(string=query)
q = q.order_by(similarity, asc=False)
except AttributeError:
raise HTTPException(
Expand Down
14 changes: 2 additions & 12 deletions examples/integrations/crewai_agentic_rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,9 @@
@tool
def website_knowledge_tool(search_query: str, limit: int = 5) -> str:
"""Search for relevant information in the website knowledge."""
chunk_view = website_knowledge.chunk_views["content"]
sim = chunk_view.text.similarity(search_query)
results = (
chunk_view.order_by(sim, asc=False)
.limit(limit)
.select(chunk_view.text, chunk_view.doc_id, similarity=sim)
.collect()
)
results = website_knowledge.search(search_query, on="content", limit=limit)
context = "\n\n".join(
[
f"Source: {r['doc_id']}\nContent: {r['text']}\nSimilarity: {r['similarity']:.4f}"
for r in results
]
f"Content: {r['text']}\nSimilarity: {r['score']:.4f}" for r in results
)
return context

Expand Down
6 changes: 4 additions & 2 deletions examples/integrations/langchain_financial_rag_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ def financial_chat(session_id: str, user_text: str, filters: dict = None) -> str
print(f"💬 User says: '{user_text}'")
rag_context = ""
if user_text:
chunk_view = kb_mem.chunk_views["report"]
# This query filters on metadata as well as ranking, which search()
# does not cover, so take the raw view handle instead.
chunk_view = kb_mem.views("report")["report"]

# Calculate similarity across all chunks
sim = chunk_view.text.similarity(user_text)
sim = chunk_view.text.similarity(string=user_text, idx="similarity")

# Start with the base view
query = chunk_view
Expand Down
1 change: 0 additions & 1 deletion examples/mcp/memory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import contextlib
import logging
from collections.abc import AsyncIterator
Expand Down
11 changes: 2 additions & 9 deletions examples/multimodal/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,8 @@
memory.add(entry)

query = "What are the key features of Pixeltable?"
chunk_view = memory.chunk_views["audio"]
sim = chunk_view.text.similarity(query)
results = (
chunk_view.order_by(sim, asc=False)
.limit(3)
.select(chunk_view.text, similarity=sim)
.collect()
)
results = memory.search(query, on="audio", limit=3)

for res in results:
print(f"Similarity: {res['similarity']:.4f}")
print(f"Similarity: {res['score']:.4f}")
print(f"Text: {res['text']}\n")
4 changes: 3 additions & 1 deletion examples/multimodal/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
memory.add(entry)

query = "A person on a skateboard"
sim = memory.image_description.similarity(query)
# This wants the image itself alongside the caption, which search() does not
# return, so query the table directly.
sim = memory.image_description.similarity(string=query, idx="similarity")
results = (
memory.order_by(sim, asc=False)
.limit(3)
Expand Down
11 changes: 2 additions & 9 deletions examples/multimodal/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,8 @@
memory.add(entry)

query = "What are the key growth drivers for Nvidia?"
chunk_view = memory.chunk_views["document"]
sim = chunk_view.text.similarity(query)
results = (
chunk_view.order_by(sim, asc=False)
.limit(3)
.select(chunk_view.text, similarity=sim)
.collect()
)
results = memory.search(query, on="document", limit=3)

for res in results:
print(f"Similarity: {res['similarity']:.4f}")
print(f"Similarity: {res['score']:.4f}")
print(f"Text: {res['text']}\n")
14 changes: 3 additions & 11 deletions examples/multimodal/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,12 @@

query = "What can you do with Pixeltable?"

# Direct similarity search on text_content column
sim = memory.text_content.similarity(query)
results = (
memory.where(sim >= 0.1)
.order_by(sim, asc=False)
.limit(3)
.select(memory.text_content, memory.memory_id, similarity=sim)
.collect()
)
results = memory.search(query, on="text_content", limit=3, min_score=0.1)

print(f"Query: '{query}'")
print("Search results:")
for res in results:
print(f"Similarity: {res['similarity']:.4f}")
print(f"Text: {res['text_content'][:100]}...")
print(f"Similarity: {res['score']:.4f}")
print(f"Text: {res['text'][:100]}...")
print(f"ID: {res['memory_id']}")
print("---")
27 changes: 7 additions & 20 deletions examples/multimodal/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,18 @@
query_audio = "What is the guest's perspective on AI?"
query_visual = "A person gesturing with their hands"

audio_chunk_view = memory.chunk_views["video"]
audio_sim = audio_chunk_view.text.similarity(query_audio)
audio_results = (
audio_chunk_view.order_by(audio_sim, asc=False)
.limit(2)
.select(audio_chunk_view.text, similarity=audio_sim)
.collect()
)
audio_results = memory.search(query_audio, on="video", limit=2)

frame_view = memory.frame_views["video"]
frame_sim = frame_view.frame_description.similarity(query_visual)
visual_results = (
frame_view.order_by(frame_sim, asc=False)
.limit(2)
.select(frame_view.frame, frame_view.frame_description, similarity=frame_sim)
.collect()
)
# Frame captions are registered separately, so visuals can be searched on their own.
visual_results = memory.search(query_visual, on="video_frames", limit=2)

print(f"\nAudio search results for: '{query_audio}'\n")
for res in audio_results:
print(f"Similarity: {res['similarity']:.4f}")
print(f"Similarity: {res['score']:.4f}")
print(f"Text: {res['text']}\n")

print(f"\nVisual search results for: '{query_visual}'\n")
for res in visual_results:
print(f"Similarity: {res['similarity']:.4f}")
print(f"Frame: {res['frame']}")
print(f"Description: {res['frame_description']}\n")
print(f"Similarity: {res['score']:.4f}")
print(f"Description: {res['text']}\n")
# For the frame images themselves, take the raw view: memory.views("video_frames")
11 changes: 2 additions & 9 deletions examples/multimodal/website.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,8 @@
memory.add(entry)

query = "inspirational quotes about life"
chunk_view = memory.chunk_views["website_content"]
sim = chunk_view.text.similarity(query)
results = (
chunk_view.order_by(sim, asc=False)
.limit(3)
.select(chunk_view.text, similarity=sim)
.collect()
)
results = memory.search(query, on="website_content", limit=3)

for res in results:
print(f"Similarity: {res['similarity']:.4f}")
print(f"Similarity: {res['score']:.4f}")
print(f"Text: {res['text']}\n")
30 changes: 22 additions & 8 deletions pixelmemory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

@dataclass
class AudioSplitterParams:
chunk_duration_sec: float = 30.0
overlap_sec: float = 0.0
min_chunk_duration_sec: float = 0.0
duration: float = 30.0
overlap: float = 0.0
min_segment_duration: float = 0.0


@dataclass
Expand All @@ -31,7 +31,7 @@ class DocumentSplitterParams:
limit: Optional[int] = 300
overlap: Optional[int] = None
metadata: str = ""
html_skip_tags: list[str] = field(default_factory=lambda: ["nav"])
skip_tags: list[str] = field(default_factory=lambda: ["nav"])
tiktoken_encoding: str = "cl100k_base"
tiktoken_target_model: Optional[str] = None

Expand Down Expand Up @@ -66,7 +66,21 @@ class FrameView:
table: pxt.Table


@dataclass
class IndexedColumn:
original_col: str
indexed_col: str
@dataclass(frozen=True)
class SearchTarget:
"""One searchable place: a column carrying an embedding index.

A memory can have several. A chunked document puts its index on the chunk
view's `text`; an image puts one on the generated description and optionally
a CLIP index on the image itself; a video contributes both its transcript
and its frame captions. `Memory.search()` walks these.

`index_name` is recorded rather than inferred because one column can carry
two indexes: chunked text is indexed on the chunk view AND directly on the
base column, so a bare `.similarity()` there would be ambiguous.
"""

context_id: str
table: pxt.Table
column: str
index_name: str
6 changes: 4 additions & 2 deletions pixelmemory/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ class Context:
embed: bool = True
embed_model: Optional[Union[str, pxt.Function]] = "all-mpnet-base-v2"
index_name: Optional[str] = None
required: bool = True
"""When False, the column accepts nulls. Pixeltable columns are non-nullable by default."""


@dataclass
class Audio(Context):
transcription_model: str = "whisper-1"
transcription_kwargs: WhisperParams = field(default_factory=WhisperParams)
chunk_params: AudioSplitterParams = field(
default_factory=lambda: AudioSplitterParams(chunk_duration_sec=30.0)
default_factory=lambda: AudioSplitterParams(duration=30.0)
)
_pxt_type: pxt.Audio = pxt.Audio

Expand Down Expand Up @@ -60,7 +62,7 @@ class Video(Context):
transcription_model: str = "whisper-1"
transcription_kwargs: WhisperParams = field(default_factory=WhisperParams)
audio_chunk_params: AudioSplitterParams = field(
default_factory=lambda: AudioSplitterParams(chunk_duration_sec=30.0)
default_factory=lambda: AudioSplitterParams(duration=30.0)
)
provider: Literal["openai", "anthropic"] = "openai"
model: str = "gpt-4o-mini"
Expand Down
Loading
Loading