An AI agent that captures every resource you share—links, notes, or mixed content—stores it in Supabase with structured tags/categories, and retrieves relevant items on demand. It is designed as a project-based learning exploration of LangGraph, tool-enabled LLMs, and local-first workflows (Ollama by default, OpenAI optional).
- Save new resources into a Supabase table with consistent columns (title, URL, notes, tags, categories).
- Update existing resources by re-saving them with new metadata (e.g., add a category later).
- Retrieve curated resource lists filtered by keywords or tags.
- Provide a simple CLI loop for experimenting with agent behaviour.
| Layer | Purpose |
|---|---|
LangGraph workflow |
Intent classification → Supabase tool execution → response formatting |
SupabaseResourceClient |
Encapsulates inserts/queries against the resources table |
pydantic-settings |
Centralises environment configuration (LLM provider + Supabase credentials) |
uv package manager |
Reproducible dependency and Python version management |
-
Install dependencies
uv sync
-
Prepare Supabase
- Create a table named
resources(or choose your own name and updateSUPABASE_RESOURCES_TABLE). - Recommended columns:
idUUID, defaultuuid_generate_v4(), primary keytitletext (required)urltext (nullable)notestext (nullable)tagstext (nullable) — store a single tag/keyword per resourcecategoriestext (nullable) — store a single category per resourcecreated_attimestamptz, defaultnow()
- Create a Service Role key (or reuse the default) and note your project URL (
https://<project>.supabase.co).
- Create a table named
-
Configure the agent
cp .env.example .env # Edit .env with: # SUPABASE_URL= # SUPABASE_KEY= # Optional: override table name with SUPABASE_RESOURCES_TABLE # Optional: switch LLM provider to openai and add OPENAI_API_KEY
-
Run the CLI
uv run python -m src.main
Example prompts:
save https://arxiv.org/abs/1234 with tags ai, researchstore “LangGraph lesson notes” under agent architecturesfind resources about prompt engineering tagged ai
Bring the same agent into Telegram for quick DM workflows.
- Create a bot via @BotFather and grab your Telegram user ID (use
@userinfobot). This ID will be the sole admin allowed to approve new chatters. - Configure environment variables (extend
.env):TELEGRAM_BOT_TOKEN=123456:bot-token TELEGRAM_ADMIN_ID=12345678 TELEGRAM_POLL_INTERVAL=0.5 TELEGRAM_PAIRING_CODE_TTL_SECONDS=3600 TELEGRAM_PAIRING_PENDING_LIMIT=3 TELEGRAM_PAIRING_STORAGE_DIR=var/pairing
- Run the bot
uv run python -m src.transport.telegram_bot
- DM pairing policy (inspired by OpenClaw)
- Unknown senders receive an 8-character pairing code (expires after 1 hour) and their message is paused until approved.
- The bot caps pending requests at 3; additional requests are ignored until one expires or is approved.
- Pairing state (pending codes + allowlist) lives under
TELEGRAM_PAIRING_STORAGE_DIR(defaultvar/pairing/, ignored via.gitignore). Point this somewhere persistent if you run the bot on a server. - Admin commands (available to the configured admin ID):
/pairing list— view pending codes/pairing approve <CODE>— allow the user and notify them/pairing reject <CODE>— delete the request and notify the user/pairing revoke <USER_ID>— remove an approved user from the allowlist
- Once approved, users interact with the same LangGraph workflow backing the CLI.
- Enable the
pgvectorextension in Supabase and add anembeddings_vector vector(1536)column to theresourcestable. - Apply the SQL in
supabase/match_resources.sql(reproduced below) to register the RPC the agent calls (note thedrop functionto replace older versions safely):drop function if exists match_resources(vector, integer, double precision, text[], text[]); create function match_resources( query_embedding vector(1536), match_count integer default 10, match_threshold double precision default 1.0, filter_tags text[] default null, filter_categories text[] default null ) returns table ( id resources.id%TYPE, title resources.title%TYPE, url resources.url%TYPE, notes resources.notes%TYPE, tags resources.tags%TYPE, categories resources.categories%TYPE, created_at resources.created_at%TYPE ) language plpgsql as $$ begin return query select r.id, r.title, r.url, r.notes, r.tags, r.categories, r.created_at from resources r where r.embeddings_vector is not null and (filter_tags is null or r.tags = any(filter_tags)) and (filter_categories is null or r.categories = any(filter_categories)) and (r.embeddings_vector <=> query_embedding) <= match_threshold order by r.embeddings_vector <=> query_embedding limit match_count; end; $$;
- The default
match_thresholdof1.0effectively keeps the top matches without filtering; lower it (e.g.,0.4) if you want to discard weaker similarities. - Set
EMBEDDING_PROVIDER,EMBEDDING_MODEL, andOPENAI_API_KEY(or Ollama equivalents) in.env. The agent automatically generates embeddings on insert/update and falls back to keyword search if embedding fails. - Backfill existing rows once after enabling embeddings:
uv run python scripts/backfill_embeddings.py --batch-size 50
- Conversation memory via LangGraph checkpointers (mirroring into Supabase)
- Automatic summaries for retrieved bundles
- Additional ingestion channels (email, RSS, read-it-later inboxes)
Development decisions and lessons learned are logged in AI.md. Each milestone explains the “why” behind the implementation to reinforce deliberate, project-based learning.