Minutes
Inspiration
We noticed a massive gap in local democracy: people don't know about bylaw and municipal changes happening that will affect their communities and lives. The current TMMIS (Toronto Meeting Management Information System) is hard to navigate and largely unbroadcasted, making it challenging for citizens to be civically active. We wanted to build an agentic civic tool that cuts through the noise and delivers real-time, actionable local updates straight to your social media feed.
What it does
Minutes runs 24/7 as an autonomous civic agent on social media. Our AI constantly monitors TMMIS and several Toronto city portals for updates, instantly digesting hours of council recordings and mountains of dense bylaw PDFs. We strip away the bureaucratic jargon, extract the insights that actually impact citizens, and automatically broadcast them as clear, engaging social media updates.
The magic is that it turns passive scrolling into immediate action. The exact same account that informs the community is how users interact with it. Users can DM the bot to automatically sign digital petitions or draft emails to local representatives, lowering the barrier to engaging in local change.
How we built it
We built the core logic using Python and deployed the application using Docker. Our system has two main pipelines:
Automated Posting
This pipeline operates autonomously, triggered either by a scheduled CRON job via GitHub Actions or on-demand via a FastAPI web server endpoint. It functions as a data-to-content assembly line.
Data Ingestion: The cycle begins with custom Python scraper classes (TMMISScraper, OpenDataClient) using BeautifulSoup4 and Requests to scrape the latest agenda items (HTML and PDFs) from Toronto City Council properties. In order to bypass complex Java-based portal security and firewall blocks, we employ a Playwright Handshake. This automated browser session handshakes with the City's authentication server to harvest sensitive XSRF-TOKEN and JSESSIONID cookies. Once handshaked, we bypass the heavy frontend entirely to hit undocumented REST APIs, returning clean JSON data. The system then regex-scans the text to find associated bylaws, dynamically triggering the BylawRegistryScraper if a match is found.
Knowledge Processing (RAG): The raw scraped text is passed to a chunking utility (rag.chunker). These chunks are embedded and stored locally in a ChromaDB vector database (CivicVectorStore). Using LangChain and OpenRouter, the bot is able to quickly search this database for pertinent context before drafting a post or answering a DM.
AI Contextual Analysis: The CivicAIAgent (connected via OpenRouter) performs semantic search queries against ChromaDB to retrieve the top 8 most relevant historical chunks. It analyzes this context to extract a highly relevant "trending" topic.
Content Generation: The AI outputs highly structured data: A 3-slide Instagram carousel (The Decision, The Numbers, The Impact), an image search keyword, an Instagram caption, and a unique tracking ID (e.g., HOUS24).
Visual Rendering: The ImageSource module uses the Pexels API to fetch a background image matching the AI's keyword. The system then boots a headless Chromium browser via Node.js/Playwright (SocialCardRenderer), injecting the AI text and Pexels image into a structured HTML/CSS template to screencap high-quality PNG/JPG assets.
Publishing & State Logging: The rendered assets are temporarily hosted publicly via the ImgBB/Catbox API. The system then invokes the Facebook/Instagram Graph API to publish the 3-slide carousel. Finally, the UID and topic context are appended to the local data/post_history.json file so that DMs can be traced to specific posts.
DM Automation & Civic Action
This pipeline acts as a persistent listener, processing inbound community engagement and routing user intent to external civic actions.
Listener & Trigger Stage: The extract_ig_dms.py script continuously polls the Facebook/Instagram Graph API for new direct messages.
State Management: When a DM is received, the system checks the text for known UIDs stored in data/post_history.json (e.g., a user replying "HOUS24"). If a match is found, the user's session is flagged as active and logged in data/conversation_state.json.
Intent Branching: The automated bot responds via the Graph API, prompting the user to choose an action path: drafting an email or signing a petition.
Path A: Automated Email Creation: * The user sends their raw, unstructured thoughts in the chat.
The CivicAIAgent ingests the messy input and uses relevant contextual chunks to structure it into a professional, persuasive email template tailored to the specific city issue.
The send_email.py script uses the Python smtplib library to fire the formatted email directly to the targeted city representative's inbox.
A confirmation message is sent back to the user via Instagram DMs.
Path B: Petition Signing:
The user replies with their Name and Postal Code.
The backend securely connects to a Supabase (PostgreSQL) database using a split security approach: we utilize highly-privileged Service Keys to enable powerful, server-side automation and data aggregation, while enforcing Row Level Security (RLS) as an isolated safety layer to ensure resident data remains cryptographically protected from unauthorized access.
It verifies if the user has already signed for this specific topic. If not, their signature data is inserted directly into the remote signatures table.
A secure confirmation message is routed back to the user via Instagram DMs.
Challenges we ran into
Building an agent that bridges clunky government infrastructure with modern social media APIs presented several hurdles:
Unpredictable DOM Structures: Government websites like TMMIS aren't always built with modern standards, making the Playwright scraping and DOM parsing a constant balancing act to ensure reliable data extraction. We were able to discover and directly hit an internal REST API on the TMMIS site by spinning a headless chromium browser to extract security cookies, saving large amounts of time and bypassing clunky HTML navigation.
Managing AI Hallucinations: Because we act as an Auto-Fact-Checking Engine, the Langchain agent had to be strictly grounded in our vector database. Ensuring the LLM generated perfectly cited responses without hallucinating municipal laws required careful prompt engineering.
Complex State Management: Handling the Instagram webhooks and routing users through seamless DM workflows—like checking UIDs and formatting emails —required robust state management. We integrated PostgreSQL as a serverless brain, storing User State Machines (such as AWAIT_EMAIL_CONTENT) and Message Bookmarks on Supabase.
Chatbot feedback loops: Early versions of the bot would sometimes re-play chat history. We solved this by writing a slicing algorithm that syncs the database bookmark with the Instagram Inbox API to calculate the exact delta of new messages.
What we learned
We learned a massive amount about integrating autonomous AI agents into existing social ecosystems. Bridging the gap between raw web scraping tools like Playwright and advanced AI frameworks like Langchain showed us how powerful automation can be for public good. We also gained deep practical experience in handling asynchronous API requests and managing webhook payloads securely.
What's next for Minutes
We have ambitious plans to scale our agentic capabilities. We want to implement the "Robo-Lobbyist", integrating with Vapi.ai or Bland AI to allow the bot to navigate phone trees and leave legally sound, synthetic voicemails expressing the user's exact concern. We also aim to add a Google Maps implementation where the bot sends a map of how changes will affect the user locally. We plan to keep the instagram page open and allow our deployed project to continue posting council updates on a regular basis.
Built With
- beautifulsoup4
- chromadb
- css3
- docker
- fastapi
- github-actions
- html5
- imgbb
- instagram-graph-api
- javascript
- langchain
- node.js
- openrouter
- pexels
- playwright
- postgresql
- pydantic
- pymupdf
- python
- react
- requests
- smtplib
- sql
- supabase
- vectordb
Log in or sign up for Devpost to join the conversation.