Files changed (3) hide show
  1. data/Dockerfile +38 -0
  2. data/README.md +117 -0
  3. data/app.R +261 -0
data/Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM rocker/shiny:4.5.2
2
+
3
+ # System dependencies for querychat/duckdb
4
+ RUN apt-get update && apt-get install -y \
5
+ libcurl4-openssl-dev \
6
+ libssl-dev \
7
+ libxml2-dev \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ WORKDIR /app
11
+
12
+ # Install remotes for version-pinned installation
13
+ RUN R -e "install.packages('remotes', repos='https://cloud.r-project.org')"
14
+
15
+ # Install CRAN packages with pinned versions
16
+ RUN R -e "remotes::install_version('shiny', version = '1.12.1', repos = 'https://cloud.r-project.org')"
17
+ RUN R -e "remotes::install_version('bslib', version = '0.10.0', repos = 'https://cloud.r-project.org')"
18
+ RUN R -e "remotes::install_version('DT', version = '0.34.0', repos = 'https://cloud.r-project.org')"
19
+ RUN R -e "remotes::install_version('dplyr', version = '1.2.0', repos = 'https://cloud.r-project.org')"
20
+ RUN R -e "remotes::install_version('RSQLite', version = '2.4.5', repos = 'https://cloud.r-project.org')"
21
+ RUN R -e "remotes::install_version('bsicons', version = '0.1.2', repos = 'https://cloud.r-project.org')"
22
+
23
+ # Install r-universe packages with pinned versions (querychat and dependencies)
24
+ RUN R -e "remotes::install_version('ellmer', version = '0.4.0', repos = c('https://posit-dev.r-universe.dev', 'https://cloud.r-project.org'))"
25
+ RUN R -e "remotes::install_version('shinychat', version = '0.3.0', repos = c('https://posit-dev.r-universe.dev', 'https://cloud.r-project.org'))"
26
+ RUN R -e "remotes::install_version('querychat', version = '0.2.0', repos = c('https://posit-dev.r-universe.dev', 'https://cloud.r-project.org'))"
27
+
28
+ # Copy application code
29
+ COPY app.R app.R
30
+ COPY data/ data/
31
+
32
+ # Verify critical packages are available
33
+ RUN R -e "stopifnot(requireNamespace('shiny', quietly = TRUE))" \
34
+ && R -e "stopifnot(requireNamespace('querychat', quietly = TRUE))"
35
+
36
+ EXPOSE 7860
37
+
38
+ CMD ["R", "--quiet", "-e", "shiny::runApp('/app', host='0.0.0.0', port=7860)"]
data/README.md ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ sdk: docker
3
+ app_port: 7860
4
+ title: ISA 401 OEWS Jobs Explorer
5
+ emoji: 📊
6
+ colorFrom: yellow
7
+ colorTo: yellow
8
+ pinned: false
9
+ license: mit
10
+ short_description: Explore U.S. occupational employment and wage data
11
+ ---
12
+
13
+ # ISA 401 OEWS Jobs Explorer
14
+
15
+ **Your AI-Powered Assistant for Exploring Occupational Employment and Wage Statistics**
16
+
17
+ [![Live App](https://img.shields.io/badge/Live_App-Hugging_Face-yellow)](https://huggingface.co/spaces/fmegahed/querychat_demo)
18
+
19
+ Explore the U.S. Bureau of Labor Statistics' Occupational Employment and Wage Statistics (OEWS) dataset using natural language queries.
20
+
21
+ ---
22
+
23
+ ## What is this app?
24
+
25
+ This Shiny application uses AI-powered natural language processing to help you explore the OEWS dataset. Instead of writing SQL queries, simply ask questions in plain English and get instant results.
26
+
27
+ **Example queries:**
28
+ - "What are the top 10 highest paying occupations in Ohio?"
29
+ - "Show me employment by industry for software developers"
30
+ - "What is the median wage for nurses nationally?"
31
+ - "Compare wages between California and Texas for data scientists"
32
+
33
+ ---
34
+
35
+ ## Dataset Information
36
+
37
+ **Dataset:** Occupational Employment and Wage Statistics (OEWS) Survey (May 2024 Estimates)
38
+ **Publisher:** U.S. Bureau of Labor Statistics (BLS), Department of Labor
39
+ **Website:** https://www.bls.gov/oes/
40
+
41
+ The OEWS program produces employment and wage estimates annually for over 800 occupations. These estimates are available for the nation as a whole, for individual states, and for metropolitan and nonmetropolitan areas.
42
+
43
+ ### Key Fields
44
+
45
+ | Field | Description |
46
+ |-------|-------------|
47
+ | `occ_title` | Occupation title |
48
+ | `area_title` | Geographic area name |
49
+ | `tot_emp` | Total employment |
50
+ | `a_mean` | Mean annual wage |
51
+ | `a_median` | Median annual wage |
52
+ | `h_mean` | Mean hourly wage |
53
+
54
+ ---
55
+
56
+ ## Features
57
+
58
+ - **Natural Language Queries**: Ask questions in plain English
59
+ - **SQL Transparency**: See the generated SQL for each query
60
+ - **Interactive Data Table**: Sort, filter, and export results
61
+ - **Miami University Theming**: Branded for ISA 401 course use
62
+
63
+ ---
64
+
65
+ ## Running Locally
66
+
67
+ **With R:**
68
+ ```r
69
+ # Install dependencies
70
+ renv::restore()
71
+
72
+ # Run the app
73
+ shiny::runApp('.', host = '0.0.0.0', port = 7860)
74
+ ```
75
+
76
+ **With Docker:**
77
+ ```bash
78
+ # Build the image
79
+ docker build -t oews-explorer .
80
+
81
+ # Run with OpenAI API key
82
+ docker run --rm -p 7860:7860 -e OPENAI_API_KEY=$OPENAI_API_KEY oews-explorer
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Required Environment Variable
88
+
89
+ This app requires an OpenAI API key to function:
90
+
91
+ ```bash
92
+ export OPENAI_API_KEY="your-api-key-here"
93
+ ```
94
+
95
+ On Hugging Face Spaces, set this as a secret in your Space settings.
96
+
97
+ ---
98
+
99
+ ## Technology Stack
100
+
101
+ - **[Shiny](https://shiny.posit.co/)** - Web application framework for R
102
+ - **[querychat](https://github.com/posit-dev/querychat)** - Natural language data querying
103
+ - **[bslib](https://rstudio.github.io/bslib/)** - Bootstrap theming for Shiny
104
+ - **[DT](https://rstudio.github.io/DT/)** - Interactive data tables
105
+
106
+ ---
107
+
108
+ ## Course Information
109
+
110
+ This application was developed for **ISA 401** at **Miami University** to help students explore and understand labor market data using modern AI-powered tools.
111
+
112
+ ---
113
+
114
+ ## Data Source
115
+
116
+ Bureau of Labor Statistics, U.S. Department of Labor. *Occupational Employment and Wage Statistics (OEWS), May 2024 Estimates.*
117
+ https://www.bls.gov/oes/
data/app.R ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ISA 401 OEWS Jobs Explorer
2
+ # A Shiny app for exploring U.S. Occupational Employment and Wage Statistics
3
+ # Built with querychat for natural language data querying
4
+
5
+ library(shiny)
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Data & QueryChat setup
9
+ # ---------------------------------------------------------------------------
10
+
11
+ oews <- as.data.frame(readRDS("data/oews.rds"))
12
+
13
+ # Pre-compute static stats for the Home tab
14
+ n_occupations <- length(unique(oews$occ_title[oews$o_group == "detailed"]))
15
+ n_areas <- length(unique(oews$area_title))
16
+ data_period <- "May 2024"
17
+
18
+ client <- ellmer::chat_openai(
19
+ model = "gpt-5-mini-2025-08-07",
20
+ credentials = function() { return(Sys.getenv("OPENAI_API_KEY")) }
21
+ )
22
+
23
+ qc <- querychat::querychat(
24
+ oews,
25
+ client = client,
26
+ greeting = "Welcome to Our ISA 401 Assistant for Understanding the OEWS Dataset",
27
+ extra_instructions = "data/extra_instructions.md",
28
+ data_description = "data/data_desc.md"
29
+ )
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # UI
33
+ # ---------------------------------------------------------------------------
34
+
35
+ ui <- bslib::page_navbar(
36
+ id = "navbar",
37
+ title = "OEWS Jobs Explorer",
38
+ theme = bslib::bs_theme(primary = "#C3142D"),
39
+ navbar_options = bslib::navbar_options(bg = "#C3142D"),
40
+ fillable = "Explorer",
41
+
42
+ sidebar = bslib::sidebar(
43
+ id = "sidebar",
44
+ width = 325,
45
+ shiny::conditionalPanel(
46
+ "input.navbar == 'Explorer'",
47
+ qc$ui()
48
+ ),
49
+ shiny::conditionalPanel(
50
+ "input.navbar == 'Home'",
51
+ tags$div(
52
+ class = "p-2",
53
+ tags$h5("Welcome!"),
54
+ tags$p("Use the tabs above to explore the data. The ",
55
+ tags$strong("Explorer"), " tab lets you query the OEWS dataset
56
+ using natural language."),
57
+ tags$hr(),
58
+ tags$p("Try asking things like:"),
59
+ tags$ul(
60
+ tags$li("Show me the top 10 highest paying occupations"),
61
+ tags$li("Filter to software developers in California"),
62
+ tags$li("What are the jobs with more than 1 million employees?")
63
+ )
64
+ )
65
+ ),
66
+ fillable = TRUE
67
+ ),
68
+
69
+ # ---------- Home Tab ----------
70
+ bslib::nav_panel(
71
+ title = "Home",
72
+ value = "Home",
73
+ icon = bsicons::bs_icon("house"),
74
+
75
+ # Hero card
76
+ bslib::card(
77
+ class = "bg-dark text-white border-0",
78
+ style = "background: linear-gradient(135deg, #C3142D 0%, #8B0E20 100%);",
79
+ bslib::card_body(
80
+ class = "text-center py-5",
81
+ tags$h1("OEWS Jobs Explorer", class = "display-4 fw-bold"),
82
+ tags$p(
83
+ class = "lead mt-3 mb-4",
84
+ "Explore U.S. Occupational Employment and Wage Statistics using natural language queries powered by AI"
85
+ ),
86
+ tags$p(class = "text-white-50 mb-4", "A Business Intelligence and Data Visualization Assignment for ISA 401 at Miami University"),
87
+ shiny::actionButton(
88
+ "go_explorer", "Start Exploring",
89
+ class = "btn btn-outline-light btn-lg px-4",
90
+ icon = shiny::icon("magnifying-glass")
91
+ )
92
+ )
93
+ ),
94
+
95
+ # App metadata row
96
+ bslib::layout_column_wrap(
97
+ width = 1 / 4,
98
+ fill = FALSE,
99
+ bslib::value_box(
100
+ title = "Version",
101
+ value = "0.1.0",
102
+ showcase = bsicons::bs_icon("tag"),
103
+ theme = "light"
104
+ ),
105
+ bslib::value_box(
106
+ title = "Last Updated",
107
+ value = "Feb 2026",
108
+ showcase = bsicons::bs_icon("calendar-check"),
109
+ theme = "light"
110
+ ),
111
+ bslib::value_box(
112
+ title = "By",
113
+ value = tags$span("Fadel M. Megahed", style = "font-size: 1em;"),
114
+ p("Miami University"),
115
+ showcase = bsicons::bs_icon("person"),
116
+ theme = "light"
117
+ ),
118
+ bslib::value_box(
119
+ title = "Data Period",
120
+ value = data_period,
121
+ p(format(n_occupations, big.mark = ","), " occupations \u00B7 ",
122
+ format(n_areas, big.mark = ","), " areas"),
123
+ showcase = bsicons::bs_icon("bar-chart-line"),
124
+ theme = "light"
125
+ )
126
+ ),
127
+
128
+ # Developer / About + Tutorial row
129
+ bslib::layout_columns(
130
+ col_widths = c(6, 6),
131
+ fill = FALSE,
132
+
133
+ bslib::card(
134
+ bslib::card_header(class = "fw-bold", bsicons::bs_icon("info-circle"), " About"),
135
+ bslib::card_body(
136
+ tags$p(
137
+ "This app uses the ",
138
+ tags$a("querychat", href = "https://posit-dev.github.io/querychat/",
139
+ target = "_blank"),
140
+ " R package to let you query the ",
141
+ tags$strong("OEWS May 2024"),
142
+ " dataset from the ",
143
+ tags$a("Bureau of Labor Statistics",
144
+ href = "https://www.bls.gov/oes/tables.htm", target = "_blank"),
145
+ " using plain English. Under the hood, your questions are translated
146
+ to SQL by an OpenAI language model."
147
+ ),
148
+ tags$hr(),
149
+ tags$h6(class = "fw-bold mb-2", "Developer"),
150
+ tags$div(
151
+ class = "d-flex align-items-start gap-3",
152
+ bsicons::bs_icon("person-circle", size = "2em"),
153
+ tags$div(
154
+ tags$div(class = "fw-semibold", "Fadel M. Megahed"),
155
+ tags$small(class = "text-muted d-block",
156
+ "Raymond E. Glos Professor, Farmer School of Business"),
157
+ tags$small(class = "text-muted d-block mb-2", "Miami University"),
158
+ tags$div(
159
+ class = "d-flex flex-wrap gap-2",
160
+ tags$a(class = "btn btn-outline-secondary btn-sm",
161
+ href = "mailto:fmegahed@miamioh.edu",
162
+ bsicons::bs_icon("envelope"), " Email"),
163
+ tags$a(class = "btn btn-outline-secondary btn-sm",
164
+ href = "https://www.linkedin.com/in/fadel-megahed-289046b4/",
165
+ target = "_blank",
166
+ bsicons::bs_icon("linkedin"), " LinkedIn"),
167
+ tags$a(class = "btn btn-outline-secondary btn-sm",
168
+ href = "https://miamioh.edu/fsb/directory/?up=/directory/megahefm",
169
+ target = "_blank",
170
+ bsicons::bs_icon("globe"), " Website"),
171
+ tags$a(class = "btn btn-outline-secondary btn-sm",
172
+ href = "https://github.com/fmegahed/",
173
+ target = "_blank",
174
+ shiny::icon("github"), " GitHub")
175
+ )
176
+ )
177
+ ),
178
+ tags$hr(),
179
+ tags$h6(class = "fw-bold mb-2", "Data Source"),
180
+ tags$div(
181
+ class = "d-flex flex-wrap gap-2",
182
+ tags$a(class = "btn btn-outline-primary btn-sm",
183
+ href = "https://www.bls.gov/oes/tables.htm", target = "_blank",
184
+ bsicons::bs_icon("table"), " OEWS Tables"),
185
+ tags$a(class = "btn btn-outline-primary btn-sm",
186
+ href = "https://www.bls.gov/oes/special-requests/oesm24all.zip",
187
+ target = "_blank",
188
+ bsicons::bs_icon("download"), " Raw Data (ZIP)")
189
+ )
190
+ )
191
+ ),
192
+
193
+ bslib::card(
194
+ bslib::card_header(class = "fw-bold", bsicons::bs_icon("play-circle"), " Video Tutorial"),
195
+ bslib::card_body(
196
+ fillable = FALSE,
197
+ tags$div(
198
+ class = "ratio ratio-16x9",
199
+ tags$iframe(
200
+ src = "https://www.youtube.com/embed/y98ssTHK4RQ",
201
+ allowfullscreen = NA,
202
+ allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
203
+ )
204
+ )
205
+ )
206
+ )
207
+ )
208
+ ),
209
+
210
+ # ---------- Explorer Tab ----------
211
+ bslib::nav_panel(
212
+ title = "Explorer",
213
+ value = "Explorer",
214
+ icon = bsicons::bs_icon("search"),
215
+
216
+ bslib::card(
217
+ bslib::card_header(shiny::textOutput("explorer_table_title")),
218
+ DT::DTOutput("table")
219
+ ),
220
+
221
+ bslib::accordion(
222
+ open = FALSE,
223
+ bslib::accordion_panel(
224
+ title = "SQL Query",
225
+ icon = bsicons::bs_icon("code-square"),
226
+ shiny::verbatimTextOutput("sql")
227
+ )
228
+ )
229
+ )
230
+ )
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Server
234
+ # ---------------------------------------------------------------------------
235
+
236
+ server <- function(input, output, session) {
237
+ qc_vals <- qc$server()
238
+
239
+ # Navigate to Explorer tab on button click
240
+ shiny::observeEvent(input$go_explorer, {
241
+ bslib::nav_select("navbar", "Explorer")
242
+ })
243
+
244
+ output$explorer_table_title <- shiny::renderText({
245
+ qc_vals$title() %||% "OEWS Data"
246
+ })
247
+
248
+ output$table <- DT::renderDT({
249
+ DT::datatable(
250
+ qc_vals$df(),
251
+ fillContainer = TRUE,
252
+ options = list(scrollX = TRUE, pageLength = 15)
253
+ )
254
+ })
255
+
256
+ output$sql <- shiny::renderText({
257
+ qc_vals$sql() %||% "SELECT * FROM oews"
258
+ })
259
+ }
260
+
261
+ shiny::shinyApp(ui, server)