{"kind":"post","path":"/blog/canada-post-ai-assisted-bi","post":{"metadata":{"title":"Canada Post Conversational BI","summary":"A four-person 2024 capstone I led: a BI system that turned plain-language questions into SQL-backed tables, charts, and follow-up analysis.","publishedAt":"2024-03-27","projectDate":"2024-05-09","ascent":"#CA261A","links":[{"label":"LinkedIn post","href":"https://lnkd.in/p/gnQrrgWJ","type":"social"}],"tags":["ai","datascience","openai","postgresql","react","nodejs","webdev","fullstack","college","bisi"],"weight":10},"slug":"canada-post-ai-assisted-bi","content":"This four-person **Business Intelligence and System Infrastructure** capstone started at Algonquin College in February 2024.\n\n\u003cblockquote className=\"bp-ownership-quote\"\u003e\n \u003cp\u003eI led the team, split ownership across data, backend, React, and prompting, then brought it together around one goal: useful reports without writing SQL.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n## What Canada Post BI Does\n\n\u003cdiv className=\"bp-capability-grid\"\u003e\n \u003cdiv className=\"bp-capability-card\" data-mark=\"01\"\u003e\n \u003cspan className=\"bp-capability-key\"\u003e01 · Plain Language\u003c/span\u003e\n \u003ch3\u003eAsk A Business Question\u003c/h3\u003e\n \u003cp\u003eUse plain language and optional filters. The app turns the request into a report plan.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-capability-card\" data-mark=\"02\"\u003e\n \u003cspan className=\"bp-capability-key\"\u003e02 · SQL And Charts\u003c/span\u003e\n \u003ch3\u003eBuild A Live Report\u003c/h3\u003e\n \u003cp\u003eThe server runs the generated PostgreSQL query and returns a table with a bar or line chart.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-capability-card\" data-mark=\"03\"\u003e\n \u003cspan className=\"bp-capability-key\"\u003e03 · Follow-Up\u003c/span\u003e\n \u003ch3\u003eExplore The Next Question\u003c/h3\u003e\n \u003cp\u003eEach report can suggest the next question, so an analyst can move from a total to a cause or region.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-capability-card\" data-mark=\"04\"\u003e\n \u003cspan className=\"bp-capability-key\"\u003e04 · Query Cache\u003c/span\u003e\n \u003ch3\u003eReuse Proven Queries\u003c/h3\u003e\n \u003cp\u003eFull-text search finds saved questions and popular reports before another model call is needed.\u003c/p\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n## Agentic BI Before OpenAI's Agent Products\n\nThis timeline keeps only milestones that changed the same workflow: plan a query, run data work, and return a usable report.\n\n\u003cdiv className=\"bp-product-timeline\" role=\"list\" aria-label=\"OpenAI data analysis products and the Canada Post BI project from 2023 to 2026\"\u003e\n \u003cdiv className=\"bp-product-timeline-item\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eMar 2023\u003c/span\u003e\n \u003ch3\u003e\u003ca href=\"https://openai.com/index/introducing-chatgpt-and-whisper-apis/\"\u003eGPT-3.5 Turbo API\u003c/a\u003e\u003c/h3\u003e\n \u003cp\u003eOpenAI released the low-cost chat model that later powered our report planner.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-product-timeline-item\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eJun 2023\u003c/span\u003e\n \u003ch3\u003e\u003ca href=\"https://openai.com/index/function-calling-and-other-api-updates/\"\u003eFunction Calling\u003c/a\u003e\u003c/h3\u003e\n \u003cp\u003eOpenAI added JSON-schema function calls. Apps could turn natural language into structured tool or database requests.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-product-timeline-item\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eJul 2023\u003c/span\u003e\n \u003ch3\u003e\u003ca href=\"https://help.openai.com/en/articles/6825453-chatgpt-release-notes\"\u003eCode Interpreter\u003c/a\u003e\u003c/h3\u003e\n \u003cp\u003eChatGPT Plus gained file analysis, Python execution, charts, math, and file editing.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-product-timeline-item\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eNov 2023\u003c/span\u003e\n \u003ch3\u003e\u003ca href=\"https://openai.com/index/new-models-and-developer-products-announced-at-devday/\"\u003eAssistants API And GPTs\u003c/a\u003e\u003c/h3\u003e\n \u003cp\u003eOpenAI combined instructions, retrieval, Code Interpreter, and function calling for purpose-built assistants.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-product-timeline-item is-project\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eFeb 2024\u003c/span\u003e\n \u003ch3\u003eCanada Post BI\u003c/h3\u003e\n \u003cp\u003eOur app used GPT-3.5 Turbo to plan SQL and charts, query PostgreSQL, stream a report, and suggest the next question.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-product-timeline-item\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eMay 2024\u003c/span\u003e\n \u003ch3\u003e\u003ca href=\"https://openai.com/index/improvements-to-data-analysis-in-chatgpt/\"\u003eInteractive Data Analysis\u003c/a\u003e\u003c/h3\u003e\n \u003cp\u003eChatGPT added connected files, expandable tables, interactive charts, and chart downloads.\u003c/p\u003e\n \u003c/div\u003e\n \u003cdiv className=\"bp-product-timeline-item\" role=\"listitem\"\u003e\n \u003cspan className=\"bp-product-timeline-date\"\u003eJan 2026\u003c/span\u003e\n \u003ch3\u003e\u003ca href=\"https://openai.com/index/inside-our-in-house-data-agent/\"\u003eOpenAI Data Agent\u003c/a\u003e\u003c/h3\u003e\n \u003cp\u003eOpenAI showed its internal agent for company data, business context, analysis, and reliable insight.\u003c/p\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\nThe capstone was narrow and purpose-built. Its core loop was already clear: ask in plain language, choose the data work, and return a report ready to use.\n\n## Visual Proof\n\n\u003cdiv className=\"bp-evidence-grid\" data-preview-gallery=\"true\"\u003e\n\u003cimg src=\"/images/blog/canada-post-ai-assisted-bi/v2-search.png\" alt=\"Business question search and saved report suggestions\" /\u003e\n\u003cimg src=\"/images/blog/canada-post-ai-assisted-bi/v2-report.png\" alt=\"Work centre failure report with chart and ranked table\" /\u003e\n\u003cimg src=\"/images/blog/canada-post-ai-assisted-bi/v2-query-help.png\" alt=\"Query refinement controls and report preview\" /\u003e\n\u003cimg src=\"/images/blog/canada-post-ai-assisted-bi/v2-settings.png\" alt=\"Model, data, and session settings\" /\u003e\n\u003c/div\u003e\n\n## Report Flow\n\n```mermaid\nflowchart LR\n accTitle: Canada Post BI Report Architecture\n accDescr: A question moves from the React client through the Node.js server, a saved query or model plan, PostgreSQL, and a live report.\n client[\"\u003cstrong\u003eReact Client\u003c/strong\u003e\u003cbr/\u003e\u003csmall\u003eQuestion And Filters\u003c/small\u003e\"] --\u003e server[\"\u003cstrong\u003eExpress + Socket.IO\u003c/strong\u003e\u003cbr/\u003e\u003csmall\u003eSearch + Report Events\u003c/small\u003e\"]\n server --\u003e plan[\"\u003cstrong\u003eCache Or Model\u003c/strong\u003e\u003cbr/\u003e\u003csmall\u003eReuse SQL Or Plan A Report\u003c/small\u003e\"]\n plan --\u003e data[\"\u003cstrong\u003ePostgreSQL\u003c/strong\u003e\u003cbr/\u003e\u003csmall\u003eRun The Query\u003c/small\u003e\"]\n data --\u003e result[\"\u003cstrong\u003eLive Report\u003c/strong\u003e\u003cbr/\u003e\u003csmall\u003eRows + Chart + Follow-Up\u003c/small\u003e\"]\n```\n\nThe React app uses the Express API for filters, autocomplete, and frequent reports. It uses Socket.IO for report loading states and final report data.\n\nThe main OpenAI adapter used `gpt-3.5-turbo` with forced function calling and a low temperature. A local DeepSeek Coder adapter was also available.\n\nPostgreSQL stores the master data and weighted full-text cache. The server executes the chosen query and sends rows and chart data back. The source data stays behind the server.\n\n## Failure Volume Fell Faster Than Parcel Volume\n\nThe live chart uses separate zero-based scales, so each series keeps its real shape without a misleading second axis. Jul 24 carried the most work and failures.\n\n\u003cdiv\n className=\"bp-paired-trend-chart\"\n data-labels=\"Jul 23|Jul 24|Jul 25|Jul 26|Jul 27|Jul 28|Jul 29\"\n data-primary-label=\"Parcel Volume\"\n data-primary-values=\"71382|519040|495333|458266|431890|294387|111677\"\n data-secondary-label=\"Failure Volume\"\n data-secondary-values=\"4679|39283|28288|19668|16750|7440|3061\"\n\u003e\u003c/div\u003e\n\n| Day | Parcel Volume | Failure Volume |\n|---|---:|---:|\n| Jul 23 | 71,382 | 4,679 |\n| Jul 24 | 519,040 | 39,283 |\n| Jul 25 | 495,333 | 28,288 |\n| Jul 26 | 458,266 | 19,668 |\n| Jul 27 | 431,890 | 16,750 |\n| Jul 28 | 294,387 | 7,440 |\n| Jul 29 | 111,677 | 3,061 |\n\n## Searchable Query Cache\n\nEach saved question had a weighted search vector. Prime questions ranked first, annotations added context, and execution counts raised common questions.\n\n```sql\n\"query_annotation\" text,\n\"execution_count\" int4 DEFAULT 1,\n\"prime_query\" bool DEFAULT false,\n\"search_vector\" tsvector GENERATED ALWAYS AS (\n setweight(to_tsvector('english', coalesce(query, '')), 'A') ||\n setweight(to_tsvector('english', coalesce(\"query_annotation\", '')), 'B') ||\n setweight(to_tsvector('english', coalesce(\"execution_count\"::text, '')), 'C') ||\n setweight(to_tsvector('english', CASE WHEN prime_query THEN 'true' ELSE 'false' END), 'D')\n) STORED,\n```\n\nThis cache reduced repeat model calls and made common questions appear sooner.\n\n### GPT-3.5 Turbo Planned The Report\n\nThe OpenAI call forced one `report_generator` function. The function returned data SQL, optional chart SQL, chart fields, and follow-up questions as structured JSON.\n\n```ts\nconst data = await openai.chat.completions.create({\n messages,\n model: \"gpt-3.5-turbo\",\n temperature: 0.08,\n tools,\n tool_choice: {\n type: \"function\",\n function: { name: \"report_generator\" },\n },\n});\n```\n\n### Socket.IO Sent Partial And Final Results\n\nThe server first marked both views as loading. It then sent table rows, chart data, chart fields, and follow-up questions in the final event.\n\n```ts\nsocket.emit(\"message_loading\", {\n data_loading: true,\n chart_loading: true,\n});\n\nconst report_data = await pool.query(ai_result.sql_data.replace(\";\", \"\"));\n\nsocket.emit(\"message_to_client\", {\n success: true,\n report_data: report_data.rows,\n report_chart: {\n chart_type: CHART_TYPE[ai_result.report_chart.chart_type],\n xField: ai_result.report_chart.xField,\n yField: ai_result.report_chart.yField,\n chart_data: report_chart.rows,\n },\n suggestions: ai_result.suggestions,\n});\n```\n\nThe capstone ran against a controlled sample database. A production version should validate generated SQL and use a read-only PostgreSQL role before execution.\n\n## Results\n\n- The app returned limited reports in less than one second in most tests.\n- Larger tests stayed below five seconds in most cases.\n- The test database held more than 600,000 rows.\n- The test server had four ARM64 CPU cores and 24 GB of memory.\n- Model API use cost less than US$0.50 during the test phase.\n\nThe model layer was replaceable. This kept provider-specific setup outside the report flow."}}