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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ license: Apache-2.0
allowed-tools: []
metadata:
author: Pixeltable
version: 2.10.0
version: 2.10.1
type: documentation
executes-code: false
category: data-infrastructure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ The cell is set to `None`, nothing raises, and `errormsg` stays empty. This is b
```python
@pxt.udf
def label(severity: str | None) -> str:
return payload or 'unknown'
return severity or 'unknown'
```

Binding a nullable argument to a non-nullable parameter also widens the column's declared type, so `excerpt(Docs.body)` over `body: pxt.String | None` is a `String | None` column.
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --locked --group dev
Expand All @@ -32,8 +32,8 @@ jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.12"
cache: npm
Expand All @@ -50,8 +50,8 @@ jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
python-version: "3.11"
- run: uv sync --locked --group dev
Expand All @@ -67,8 +67,8 @@ jobs:
pixeltable-schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
python-version: "3.11"
- run: uv sync --locked --group dev
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Pixelbot 3.0 is an app-first Pixeltable 0.7.7 project. Python 3.11+ and Node 22.
- `backend/pixelbot/routers/`: custom REST routes. The database router is read-only.
- `backend/pixelbot/static/`: production SPA package data generated by Vite.
- `frontend/`: React SPA with route-level lazy loading.
- `.agents/skills/pixeltable-skill/`: bundled current Pixeltable guidance.
- `.agents/skills/pixeltable/`: a reproducible project-local snapshot of the canonical
`pixeltable/pixeltable-skill` repository. `skills-lock.json` records its source and content hash;
keep both in sync when refreshing the skill.

## Catalog rules

Expand Down
9 changes: 9 additions & 0 deletions backend/pixelbot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ def user_info() -> dict[str, str]:
return {"user_name": config.DEFAULT_USER_NAME}


@app.api_route(
"/api/{unmatched_path:path}",
methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
include_in_schema=False,
)
async def unmatched_api_route(unmatched_path: str) -> JSONResponse:
return JSONResponse(status_code=404, content={"detail": "Not Found"})


staticDir = Path(__file__).resolve().parent / "static"

if staticDir.is_dir():
Expand Down
4 changes: 2 additions & 2 deletions backend/pixelbot/routers/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ def join_tables(body: JoinRequest):
"search_documents": "pixelbot_v3/chunks",
"search_images": "pixelbot_v3/images",
"search_video_frames": "pixelbot_v3/video_frames",
"search_video_transcripts": "pixelbot_v3/video_transcript_sentences",
"search_audio_transcripts": "pixelbot_v3/audio_transcript_sentences",
"search_video_transcripts": "pixelbot_v3/video_audio_chunks",
"search_audio_transcripts": "pixelbot_v3/audio_chunks",
"search_memory": "pixelbot_v3/memory_bank",
"search_chat_history": "pixelbot_v3/chat_history",
"get_recent_chat_history": "pixelbot_v3/chat_history",
Expand Down
16 changes: 8 additions & 8 deletions backend/pixelbot/routers/studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ def search_studio(body: SearchRequest):

# Also search video transcripts
try:
vt_view = pxt.get_table("pixelbot_v3.video_transcript_sentences")
vt_view = pxt.get_table("pixelbot_v3.video_audio_chunks")
sim = vt_view.text.similarity(string=body.query)
for row in (
vt_view.where((vt_view.user_id == user_id) & (sim > body.threshold))
Expand All @@ -1031,7 +1031,7 @@ def search_studio(body: SearchRequest):
# ── Audio transcripts (Gemini embed) ──
if "audio" in body.types:
try:
at_view = pxt.get_table("pixelbot_v3.audio_transcript_sentences")
at_view = pxt.get_table("pixelbot_v3.audio_chunks")
sim = at_view.text.similarity(string=body.query)
for row in (
at_view.where((at_view.user_id == user_id) & (sim > body.threshold))
Expand Down Expand Up @@ -1190,9 +1190,9 @@ def _collect_text_embeddings(
except Exception as e:
logger.error(f"Embedding viz: document error: {e}")

# Video transcript sentences
# Video transcript chunks
try:
vt_view = pxt.get_table("pixelbot_v3.video_transcript_sentences")
vt_view = pxt.get_table("pixelbot_v3.video_audio_chunks")
for row in (
vt_view.where(vt_view.user_id == user_id)
.select(
Expand All @@ -1216,9 +1216,9 @@ def _collect_text_embeddings(
except Exception as e:
logger.error(f"Embedding viz: video transcript error: {e}")

# Audio transcript sentences
# Audio transcript chunks
try:
at_view = pxt.get_table("pixelbot_v3.audio_transcript_sentences")
at_view = pxt.get_table("pixelbot_v3.audio_chunks")
for row in (
at_view.where(at_view.user_id == user_id)
.select(
Expand Down Expand Up @@ -2280,9 +2280,9 @@ def get_transcription(uuid: str, media_type: str):

try:
if media_type == "audio":
view_name = "pixelbot_v3.audio_transcript_sentences"
view_name = "pixelbot_v3.audio_chunks"
else:
view_name = "pixelbot_v3.video_transcript_sentences"
view_name = "pixelbot_v3.video_audio_chunks"

view = pxt.get_table(view_name)
sentences = []
Expand Down
75 changes: 39 additions & 36 deletions backend/pixelbot/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from __future__ import annotations

from typing import Any

import pixeltable as pxt
from pixeltable.functions import bfl, gemini, openai
from pixeltable.functions import image as pxt_image
Expand All @@ -14,7 +16,6 @@
from pixeltable.functions.document import document_splitter
from pixeltable.functions.gemini import invoke_tools
from pixeltable.functions.huggingface import clip
from pixeltable.functions.string import string_splitter
from pixeltable.functions.video import extract_audio, frame_iterator

from pixelbot import config, functions
Expand All @@ -26,6 +27,31 @@
clipEmbed = clip.using(model_id=config.CLIP_MODEL_ID)


def _jsonb_stable(value: Any) -> Any:
"""Match PostgreSQL JSONB key ordering for stable schema reconciliation."""
if isinstance(value, dict):
return {key: _jsonb_stable(value[key]) for key in sorted(value, key=lambda key: (len(key), key))}
if isinstance(value, list):
return [_jsonb_stable(item) for item in value]
return value


DOCUMENT_SUMMARY_CONFIG = _jsonb_stable(
{
"system_instruction": "Analyze the document text and return a structured summary.",
"response_mime_type": "application/json",
"response_schema": DocumentSummary.model_json_schema(),
}
)
FOLLOW_UP_CONFIG = _jsonb_stable(
{
"system_instruction": "Generate exactly 3 relevant follow-up questions based on the conversation.",
"response_mime_type": "application/json",
"response_schema": FollowUpResponse.model_json_schema(),
}
)


class Documents(TableModel, name="collection", has_default_idxs=False):
document: pxt.Document
uuid = pxt.Column(type=pxt.String, primary_key=True)
Expand All @@ -35,11 +61,7 @@ class Documents(TableModel, name="collection", has_default_idxs=False):
summary_response = gemini.generate_content(
contents=document_text,
model=config.GEMINI_MODEL_ID,
config={
"system_instruction": "Analyze the document text and return a structured summary.",
"response_mime_type": "application/json",
"response_schema": DocumentSummary.model_json_schema(),
},
config=DOCUMENT_SUMMARY_CONFIG,
)
summary = summary_response.candidates[0].content.parts[0].text
__indexes__ = [pxt.BtreeIndex(user_id), pxt.BtreeIndex(timestamp)]
Expand Down Expand Up @@ -157,27 +179,19 @@ class VideoAudioChunks(
has_default_idxs=False,
):
transcription = openai.transcriptions(audio=audio, model=config.WHISPER_MODEL_ID) # type: ignore[name-defined]


class VideoTranscriptSentences(
TableModel,
name="video_transcript_sentences",
base=VideoAudioChunks.where(VideoAudioChunks.transcription != None), # noqa: E711
iterator=string_splitter(VideoAudioChunks.transcription.text, separators="sentence"),
has_default_idxs=False,
):
text = transcription.text
__indexes__ = [
pxt.EmbeddingIndex(text, string_embed=geminiEmbed, name="video_sentences_gemini"), # type: ignore[name-defined]
]


@pxt.query
def search_video_transcripts(query_text: str, user_id: str = config.DEFAULT_USER_ID):
sim = VideoTranscriptSentences.text.similarity(string=query_text)
sim = VideoAudioChunks.text.similarity(string=query_text)
return (
VideoTranscriptSentences.where((VideoTranscriptSentences.user_id == user_id) & (sim > 0.7))
VideoAudioChunks.where((VideoAudioChunks.user_id == user_id) & (sim > 0.7))
.order_by(sim, asc=False)
.select(VideoTranscriptSentences.text, source_video=VideoTranscriptSentences.video, sim=sim)
.select(VideoAudioChunks.text, source_video=VideoAudioChunks.video, sim=sim)
.limit(20)
)

Expand All @@ -198,27 +212,19 @@ class AudioChunks(
has_default_idxs=False,
):
transcription = openai.transcriptions(audio=audio, model=config.WHISPER_MODEL_ID) # type: ignore[name-defined]


class AudioTranscriptSentences(
TableModel,
name="audio_transcript_sentences",
base=AudioChunks.where(AudioChunks.transcription != None), # noqa: E711
iterator=string_splitter(AudioChunks.transcription.text, separators="sentence"),
has_default_idxs=False,
):
text = transcription.text
__indexes__ = [
pxt.EmbeddingIndex(text, string_embed=geminiEmbed, name="audio_sentences_gemini"), # type: ignore[name-defined]
]


@pxt.query
def search_audio_transcripts(query_text: str, user_id: str = config.DEFAULT_USER_ID):
sim = AudioTranscriptSentences.text.similarity(string=query_text)
sim = AudioChunks.text.similarity(string=query_text)
return (
AudioTranscriptSentences.where((AudioTranscriptSentences.user_id == user_id) & (sim > 0.6))
AudioChunks.where((AudioChunks.user_id == user_id) & (sim > 0.6))
.order_by(sim, asc=False)
.select(AudioTranscriptSentences.text, source_audio=AudioTranscriptSentences.audio, sim=sim)
.select(AudioChunks.text, source_audio=AudioChunks.audio, sim=sim)
.limit(30)
)

Expand Down Expand Up @@ -427,6 +433,7 @@ class Notifications(TableModel, name="notifications", has_default_idxs=False):
search_video_transcripts,
search_audio_transcripts,
)
agentToolDeclarations = _jsonb_stable(agentTools.ser_model())


class ToolAgent(TableModel, name="tools", has_default_idxs=False):
Expand All @@ -442,7 +449,7 @@ class ToolAgent(TableModel, name="tools", has_default_idxs=False):
initial_response = gemini.generate_content(
contents=tool_selection_messages,
model=config.GEMINI_MODEL_ID,
tools=agentTools,
tools=agentToolDeclarations,
config={"system_instruction": initial_system_prompt, "temperature": temperature},
)
tool_output = invoke_tools(agentTools, initial_response)
Expand Down Expand Up @@ -474,11 +481,7 @@ class ToolAgent(TableModel, name="tools", has_default_idxs=False):
follow_up_raw_response = gemini.generate_content(
contents=follow_up_input_message,
model=config.GEMINI_MODEL_ID,
config={
"system_instruction": "Generate exactly 3 relevant follow-up questions based on the conversation.",
"response_mime_type": "application/json",
"response_schema": FollowUpResponse.model_json_schema(),
},
config=FOLLOW_UP_CONFIG,
)
follow_up_text = follow_up_raw_response.candidates[0].content.parts[0].text
__indexes__ = [pxt.BtreeIndex(user_id), pxt.BtreeIndex(timestamp)]

Large diffs are not rendered by default.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
import{c as o}from"./index-bcIN4R_6.js";const r=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]],c=o("arrow-right",r);export{c as A};
import{c as o}from"./index-BKlAZSPs.js";const r=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]],c=o("arrow-right",r);export{c as A};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading