Scaffold locally · backend
Chat agent
Tool-calling agent with persistent memory, a knowledge base, and conversation history.
Scaffold locally
uvx pixeltable-new --template chat-agent my-chat-agentSame starter-kit files Cloud uses. Local UI (static HTML in some templates) is for uvx, not for Cloud. Cloud deploys schema + insert routes via pxt serve.
Secrets
Set these on the database before calling model-backed routes: OPENAI_API_KEY
schema.py
"""Persistent multimodal agent — everything is a table.
Replaces Mem0/MemGPT: conversations, memory, knowledge, and tool traces
are all queryable, versioned Pixeltable tables.
python app.py # start the server with web UI
# or API-only: python schema.py && pxt serve agent
"""
import os
from datetime import datetime
import functions
import pixeltable as pxt
from pixeltable.functions.huggingface import sentence_transformer
from pixeltable.functions.string import string_splitter
from pixeltable.functions.uuid import uuid7
HAS_ANTHROPIC = bool(os.getenv("ANTHROPIC_API_KEY"))
if HAS_ANTHROPIC:
from pixeltable.functions.anthropic import invoke_tools, messages
pxt.create_dir("agent", if_exists="ignore")
embed_fn = sentence_transformer.using(model_id="all-MiniLM-L6-v2")
# ── Knowledge base ────────────────────────────────────────────────────────
knowledge = pxt.create_table(
"agent.knowledge",
{
"text": pxt.String,
"title": pxt.String,
"source": pxt.String,
"uuid": uuid7(),
"timestamp": pxt.Timestamp,
},
primary_key=["uuid"],
if_exists="ignore",
)
sentences = pxt.create_view(
"agent.sentences",
knowledge,
iterator=string_splitter(text=knowledge.text, separators="sentence"),
if_exists="ignore",
)
sentences.add_embedding_index("text", idx_name="knowledge_embed", string_embed=embed_fn, if_exists="ignore")
# ── Conversations / memory ────────────────────────────────────────────────
conversations = pxt.create_table(
"agent.conversations",
{
"role": pxt.String,
"content": pxt.String,
"conversation_id": pxt.String,
"user_id": pxt.String,
"uuid": uuid7(),
"timestamp": pxt.Timestamp,
},
primary_key=["uuid"],
if_exists="ignore",
)
conversations.add_embedding_index("content", idx_name="conversations_embed", string_embed=embed_fn, if_exists="ignore")
# ── Query functions ───────────────────────────────────────────────────────
@pxt.query
def search_knowledge(query_text: str, limit: int = 10):
"""Semantic search over the knowledge base."""
sim = sentences.text.similarity(string=query_text)
return (
sentences.where(sim > 0.3)
.order_by(sim, asc=False)
.select(sentences.text, title=sentences.title, score=sim)
.limit(limit)
)
@pxt.query
def recall_memory(query_text: str, limit: int = 10):
"""Semantic recall across all conversations — long-term memory."""
sim = conversations.content.similarity(string=query_text)
return (
conversations.where(sim > 0.5)
.order_by(sim, asc=False)
.select(
role=conversations.role,
content=conversations.content,
conversation_id=conversations.conversation_id,
score=sim,
)
.limit(limit)
)
@pxt.query
def get_history(conversation_id: str, limit: int = 10):
"""Recent turns from a specific conversation."""
return (
conversations.where(conversations.conversation_id == conversation_id)
.order_by(conversations.timestamp, asc=False)
.select(
role=conversations.role,
content=conversations.content,
timestamp=conversations.timestamp,
)
.limit(limit)
)
# ── Tools ─────────────────────────────────────────────────────────────────
# Load tools from any MCP-compliant server:
# mcp_tools = pxt.mcp_udfs('http://localhost:8000/mcp')
# tools = pxt.tools(functions.web_search, search_knowledge, recall_memory, *mcp_tools)
# ── Agent table (always created so pxt serve can register routes) ─────────
agent = pxt.create_table(
"agent.agent",
{
"prompt": pxt.String,
"conversation_id": pxt.String,
"system_prompt": pxt.String,
"max_tokens": pxt.Int,
"temperature": pxt.Float,
"uuid": uuid7(),
"timestamp": pxt.Timestamp,
},
primary_key=["uuid"],
if_exists="ignore",
)
# ── LLM pipeline (requires ANTHROPIC_API_KEY) ────────────────────────────
if HAS_ANTHROPIC:
tools = pxt.tools(functions.web_search, search_knowledge, recall_memory)
agent.add_computed_column(memory_context=recall_memory(agent.prompt), if_exists="ignore")
agent.add_computed_column(knowledge_context=search_knowledge(agent.prompt), if_exists="ignore")
agent.add_computed_column(
initial_response=messages(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": agent.prompt}],
tools=tools,
tool_choice=tools.choice(required=True),
max_tokens=agent.max_tokens,
model_kwargs={
"system": agent.system_prompt,
"temperature": agent.temperature,
},
),
if_exists="ignore",
)
agent.add_computed_column(
tool_output=invoke_tools(tools, agent.initial_response),
if_exists="ignore",
)
agent.add_computed_column(
context=functions.assemble_context(
agent.prompt,
agent.memory_context,
agent.knowledge_context,
agent.tool_output,
),
if_exists="ignore",
)
agent.add_computed_column(
final_response=messages(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": agent.context}],
max_tokens=agent.max_tokens,
model_kwargs={
"system": agent.system_prompt,
"temperature": agent.temperature,
},
),
if_exists="ignore",
)
agent.add_computed_column(answer=agent.final_response.content[0].text, if_exists="ignore")
# ── Main agent endpoint ──────────────────────────────────────────────────
def ask(question: str, conversation_id: str = "default") -> str:
"""Insert prompt → computed column chain → answer. Saves to conversation history."""
if not HAS_ANTHROPIC:
return "Error: set ANTHROPIC_API_KEY to enable the agent pipeline."
ts = datetime.now()
agent_tbl = pxt.get_table("agent.agent")
agent_tbl.insert(
[
{
"prompt": question,
"conversation_id": conversation_id,
"system_prompt": (
"You are a helpful assistant with access to tools, a knowledge base, "
"and conversation memory. Use tools when needed. Be concise and accurate."
),
"max_tokens": 1024,
"temperature": 0.7,
"timestamp": ts,
}
]
)
result = (
agent_tbl.where(agent_tbl.timestamp == ts)
.order_by(agent_tbl.timestamp, asc=False)
.limit(1)
.select(agent_tbl.answer)
.collect()
)
if not result:
return "Error: no response generated."
answer = result[0].get("answer", "Error: no answer in response.")
conversations.insert(
[
{
"role": "user",
"content": question,
"conversation_id": conversation_id,
"user_id": "default",
"timestamp": ts,
},
{
"role": "assistant",
"content": answer,
"conversation_id": conversation_id,
"user_id": "system",
"timestamp": datetime.now(),
},
]
)
return answer
if __name__ == "__main__":
print("Schema initialized. Run: python app.py")
README
Persistent Multimodal Agent
A durable agent where conversations, memory, knowledge, and tool traces are all queryable, versioned Pixeltable tables. Your own Mem0, self-hosted.
What it replaces: Mem0 ($5K–30K/yr), MemGPT, custom agent platforms with ephemeral memory.
Key differentiator: Tables ARE the memory. Every conversation turn, tool invocation, and knowledge chunk is a row — not ephemeral, fully queryable across sessions, with complete lineage and version history. No separate vector DB, no Redis sidecar, no memory service.
Quick Start
uv sync # install deps
ANTHROPIC_API_KEY=sk-... uv run python app.py
# Open http://localhost:8000
That's it. app.py initializes the schema and starts the server with the web UI.
API-only mode (no UI)
ANTHROPIC_API_KEY=sk-... uv run python schema.py
ANTHROPIC_API_KEY=sk-... uv run pxt serve agent
Do not run both pxt serve and app.py at the same time -- they bind to the same port.
/ask endpoint differences
app.py and pxt serve agent expose different /ask contracts:
| Mode | Endpoint | Behavior |
|---|---|---|
app.py |
POST /api/ask |
Custom handler calling schema.ask(); body: {"question": "...", "conversation_id": "..."}; returns {"answer": "..."} |
pxt serve agent |
POST /api/ask |
Insert route into agent.agent; body includes prompt, conversation_id, and optional system_prompt, max_tokens, temperature; returns insert row with computed pipeline columns |
Both trigger the same computed-column agent pipeline; choose based on whether you need the simplified app handler or direct table insert semantics.
Architecture
Insert a prompt → computed columns fire in sequence → answer comes back:
INSERT {prompt, conversation_id}
│
├─ memory_context ← recall_memory(): semantic search over past conversations
├─ knowledge_context ← search_knowledge(): RAG over sentence-chunked knowledge base
│
├─ initial_response ← Anthropic messages() with tools (web_search, search, recall)
├─ tool_output ← invoke_tools() executes whichever tools the LLM chose
│
├─ context ← assemble_context() merges memory + knowledge + tool results
├─ final_response ← Anthropic messages() with full assembled context
└─ answer ← extracted response text
Every step is a column. Every column is queryable, versioned, and debuggable.
Endpoints
| Method | Path | Type | Description |
|---|---|---|---|
POST |
/api/ask |
insert | Ask the agent (triggers full computed column chain) |
POST |
/api/knowledge |
insert | Add a document to the knowledge base |
GET |
/api/knowledge/search |
query | Semantic search over the knowledge base |
GET |
/api/memory/search |
query | Semantic search across all conversations |
GET |
/api/history |
query | Get recent turns from a conversation |
Tables
| Table | Purpose |
|---|---|
agent.knowledge |
Knowledge base documents |
agent.sentences |
View: sentence-level chunks with embedding index |
agent.conversations |
All messages with role, content, conversation_id, user_id |
agent.agent |
Agent pipeline: prompt → computed column chain → answer |
Python SDK
import schema
# Add knowledge
schema.knowledge.insert([{
'text': 'Pixeltable is declarative data infrastructure for AI.',
'title': 'About Pixeltable',
'source': 'docs',
}])
# Ask the agent (inserts into agent table, saves conversation history)
answer = schema.ask('What is Pixeltable?', conversation_id='sess-1')
# Direct query access — @pxt.query functions return a DataFrame; call .collect()
results = schema.search_knowledge('data infrastructure').collect()
memories = schema.recall_memory('deployment options').collect()
history = schema.get_history('sess-1').collect()
Adding Tools
Register any @pxt.udf or @pxt.query function as a tool the agent can call:
tools = pxt.tools(web_search, search_knowledge, recall_memory, your_custom_tool)
Connect to external services via MCP:
mcp_tools = pxt.mcp_udfs('http://localhost:9000/mcp')
tools = pxt.tools(web_search, search_knowledge, *mcp_tools)
Configuration
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
Yes | Enables the agent pipeline (LLM + tools) |
PIXELTABLE_HOME |
No | Persistent storage (default: ~/.pixeltable) |
Files
chat-agent/
├── schema.py Tables, views, indexes, computed columns, tools, query functions
├── functions.py UDFs (web_search, assemble_context)
├── app.py FastAPI server — API + web UI
├── static/
│ └── index.html Frontend (Tailwind CSS, vanilla JS)
├── pyproject.toml Dependencies + pxt serve routes (API-only alternative)
└── README.md