# AGENTS.md ## Run ``` python main.py ``` Uses `runpy` to exec `bot.py`. No tests, no lint, no CI. ## Architecture - `bot.py` — Telegram bot (python-telegram-bot), single-worker `asyncio.Queue`, all crawl/parse serialized - `agent.py` — pydantic-ai with local Ollama (`granite4.1:8b`), three agents (opportunity/event/workshop), `output_type=str` - `database.py` — PocketBase client, collections `events` / `opportunities` / `workshops` - `scraper.py` — crawl4ai `AsyncWebCrawler` URL → markdown - `prompts.py` — system prompts for both agent types - `schemas.py` — **unused at runtime**, per file comment: "File doesn't do anything, its just an outline" ## Windows event-loop quirk DO NOT remove the dual-event-loop pattern in `bot.py`: 1. PTB runs on `WindowsSelectorEventLoopPolicy` (line 357) 2. Scraper runs in a **separate thread** with `WindowsProactorEventLoopPolicy` (line 25) — required for Playwright subprocesses on Windows The `get_clean_content()` wrapper in `bot.py` runs `_run_scraper_in_thread` via `ThreadPoolExecutor`. Scraper functions must not be called directly. ## Agent output format Agents use `output_type=str` (not structured output). JSON is parsed manually from `agent.run()` result: - LLM returns JSON wrapped in `````json ... ````` markdown backticks - `agent.py:56` strips the backtick wrappers before `json.loads()` - The markdown wrapper is specified in `prompts.py` — don't change prompt format without updating the strip logic ## Date formats - Prompts instruct LLM: `DD-MM-YYYY` (or `DD-MM-YYYY (HH:MM)` for events) - PocketBase expects: `YYYY-MM-DD HH:MM:SS` - `database.py` field mapping: event `date_time` → PocketBase `datetime`, `end_date` → `end_datetime`; workshop same; opportunity `deadline` stays `deadline` ## PocketBase collections - `events` — fields: `title`, `org`, `datetime`, `end_datetime`, `summary`, `location`, `url` - `opportunities` — fields: `title`, `org`, `type`, `deadline`, `summary`, `location`, `url` - `workshops` — fields: `title`, `org`, `datetime`, `end_datetime`, `summary`, `location`, `url` - Auth uses admin credentials from `.env` ## Env vars `.env` required: `TG_TOKEN`, `OLLAMA_BASE_URL`, `ALLOWED_USERS`, `POCKETBASE_URL`, `POCKETBASE_ADMIN_EMAIL`, `POCKETBASE_ADMIN_PASSWORD` ## Auth `ALLOWED_USERS` — comma-separated Telegram user IDs (no brackets). Checked on every command/message handler. ## Save flow When source is pasted text (not URL), the bot asks for a source URL before saving. `context.user_data` keys: `last_extracted`, `last_source_value`, `last_source_kind`, `last_entry_type`, `awaiting_save_url`, `pending_save_url`, `pending_url_to_process`.