Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .lintr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
linters:
linters_with_defaults(
line_length_linter = line_length_linter(100),
object_usage_linter = NULL # Does not work with `box::use()`.
defaults = box.linters::rhino_default_linters,
line_length_linter = line_length_linter(100)
)
2 changes: 0 additions & 2 deletions app/logic/empty_state_utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ box::use(
div,
img,
p,
renderUI,
tagAppendAttributes
],
)

Expand Down
3 changes: 0 additions & 3 deletions app/logic/general_utils.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
box::use(
config[
get
],
purrr[
map_chr
],
Expand Down
128 changes: 128 additions & 0 deletions app/logic/llm_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
box::use(
config[
get
],
ellmer, #nolint: we do use this package just in a separate notation [[ ]]
glue[
glue
],
)

#' Check if LLM is enabled
#'
#' This function checks the LLM configuration and returns TRUE if enabled, FALSE otherwise.
#' @return Logical indicating if LLM is enabled
#' @export
is_llm_enabled <- function() {
get("llm")$enabled %||% FALSE
}

#' Get valid LLM providers
#' @return A character vector of valid LLM providers
get_valid_providers <- function(
) {
ellmer_functions <- ls(ellmer)[
grepl(
pattern = "^chat_",
x = ls(ellmer)
)
]
sub(
pattern = "^chat_",
replacement = "",
x = ellmer_functions
)
}

#' Check if the provider is valid
#' @param provider The LLM provider to check
#' @param valid_providers A character vector of valid LLM providers
#' @return Logical indicating if the provider is valid
verify_provider <- function(
provider,
valid_providers = get_valid_providers()
) {
if (!provider %in% valid_providers) {
stop(
glue(
"Invalid LLM provider '{provider}'. ",
"Valid providers are: {paste(valid_providers, collapse = ', ')}"
)
)
}
TRUE
}

#' Get LLM configuration
#'
#' Returns the LLM configuration if LLM is enabled. Otherwise, throws an error.
#' @return A list containing the LLM configuration
get_llm_config <- function() {
if (!is_llm_enabled()) {
stop("Oops! LLM is not enabled in config.yml!")
}
verify_provider(
get("llm")$provider,
get_valid_providers()
)
get("llm")
}

#' Get the LLM function based on the provider
#'
#' Extracts the chat function dynamically from the `ellmer` module.
#' @param llm_config Optional configuration for the LLM
#' @return A function to create a chat object
get_llm_function <- function(
llm_config = get_llm_config()
) {
ellmer[[glue("chat_{llm_config$provider}")]]
}

#' Create a chat object
#'
#' Uses the configured LLM provider and model to create a chat object.
#' @param llm_config Optional configuration for the LLM
#' @return A chat object
#' @export
create_chat_object <- function(
llm_config = get_llm_config()
) {
fun <- get_llm_function(llm_config)
fun(
api_key = llm_config$api_key,
model = llm_config$model,
system_prompt = llm_config$system_prompt,
seed = 42,
api_args = list(
temperature = 0
)
)
}

#' Invoke LLM help with the logs data.frame
#' @param logs_data A data frame containing log data
#' @return A response from the LLM
#' @export
get_llm_help <- function(
logs_data
) {
chat <- create_chat_object()
chat$chat(
concatenate_logs(
logs_data
)
)
}

#' Concatenate logs
#' @param processed_logs A data frame containing log data
#' @return A string with concatenated log entries
concatenate_logs <- function(
processed_logs
) {
paste(
processed_logs$entries.data,
collapse = "\n"
)
}
18 changes: 10 additions & 8 deletions app/logic/logs_utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@ process_log_data <- function(
log_data
) {
log_info <- strsplit(log_data, "_-_")[[1]]
status <- get_status_info(log_info[1], log_info[3])
div(
class = glue("log-entry {status[1]}-highlight"),
class = glue("log-entry {log_info[4]}-highlight"),
icon(
name = status[2],
name = log_info[5],
class = glue(
"log-status {status[1]}-text fa-solid"
"log-status {log_info[4]}-text fa-solid"
),
),
div(
class = "log-info-block",
div(
class = glue("log-info {status[1]}-text"),
class = glue("log-info {log_info[4]}-text"),
log_info[3]
),
div(
Expand All @@ -41,15 +40,18 @@ process_log_data <- function(
)
}

#' @export
get_status_info <- function(
output_type,
log_data
) {
if (output_type == "stdout") {
c("green", "circle-info")
status_list <- list("green", "circle-info")
} else if (output_type == "stderr" && check_text_error(log_data)) {
c("red", "circle-xmark")
status_list <- list("red", "circle-xmark")
} else {
c("yellow", "circle-info")
status_list <- list("yellow", "circle-info")
}
names(status_list) <- c("entries.status", "entries.icon")
status_list
}
42 changes: 24 additions & 18 deletions app/main.R
Original file line number Diff line number Diff line change
Expand Up @@ -62,38 +62,44 @@ server <- function(id) {

mod_header$server("header")

selected_app_ <- mod_app_table$server("app_table", app_list)$selected_app_

selected_job_ <- mod_job_list$server("job_list", selected_app_)$selected_job_

mod_logs$server("logs", selected_app_, selected_job_)
selected_app_ <- mod_app_table$server(
"app_table",
app_list
)$selected_app_

selected_job_ <- mod_job_list$server(
"job_list",
selected_app_
)$selected_job_

mod_logs$server(
"logs",
selected_app_,
selected_job_
)

output$job_list_pane <- shiny$renderUI({
if (!shiny$isTruthy(selected_app_()$guid)) {
return(NULL)
NULL
}

mod_job_list$ui(ns("job_list"))
})

output$logs_pane <- shiny$renderUI({
if (!is.data.frame(app_list) || nrow(app_list) == 0) {
return(
generate_empty_state_ui(
text = "Oops! Can't read apps from Posit Connect.",
image_path = "app/static/illustrations/missing_apps.svg",
color = branding$colors$primary
)
generate_empty_state_ui(
text = "Oops! Can't read apps from Posit Connect.",
image_path = "app/static/illustrations/missing_apps.svg",
color = branding$colors$primary
)
}

if (!shiny$isTruthy(selected_job_()$key)) {
return(
generate_empty_state_ui(
text = "Select an application and a job to view logs.",
image_path = "app/static/illustrations/empty_state.svg",
color = branding$colors$primary
)
generate_empty_state_ui(
text = "Select an application and a job to view logs.",
image_path = "app/static/illustrations/empty_state.svg",
color = branding$colors$primary
)
}

Expand Down
2 changes: 1 addition & 1 deletion app/static/css/app.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions app/static/llm_system_prompt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Context: You are an extremely sophisticated, R/Shiny log analyzer and debugger.
Task: You will be given only the problematic logs. Your job is to analyse them.
Result: Generate a div with formatting as given below.
Structure:
- <h2><strong>LogAnalyzer AI Help</strong></h2>
- <hr>
- <p class="red-text"><strong>Caution:</strong> This help is AI generated. Verify the information before taking any action.</p>
- <h3>Problem Explanation</h3>
- [Add a one- or two-liner explanation here]
- <h3>Suggestions</hr> (bullet list of 3 suggestions, be detailed, but no subpoints)
Formatting:
This will be embedded in a modalDialog in a Shiny app. Following this to the last letter is important. Breaking these will get you fired.
- Ensure function, library and related names use <code> tags.
- All code blocks and one-liners should be formatted properly using <code> tags.
- All emphasis should happen with <em> and not _ or *.
- All bold should happen with <strong> and not ** or __.
- Return raw HTML. No \```html wrapper.
37 changes: 33 additions & 4 deletions app/styles/_logs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
padding: 0 !important;
}

.rt-search {
width: 85% !important;
align-self: flex-start !important;
}

> div {
text-align: center;

Expand All @@ -23,6 +28,27 @@
}
}

.logs-llm-modal {
.modal-dialog {
text-align: justify;

h2,
h3 {
color: var(--primary);
}

.btn {
background: var(--red-highlight);
border-radius: 0;
border: 1px solid var(--red);
}

.modal-content {
border-radius: 0;
}
}
}

.logs-container {
position: relative;

Expand Down Expand Up @@ -52,13 +78,16 @@
}
}

.logs-download {
.logs-options {
position: absolute;
z-index: 2;
right: 0;
margin: 10px;
background: 0;
border-radius: 0;
padding: 5px 10px;

.logs-options-button {
background: 0;
border-radius: 0;
padding: 5px 10px;
}
}
}
2 changes: 1 addition & 1 deletion app/view/mod_header.R
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ ui <- function(id) {
div(
class = "right header-section",
actionLink(
"lets-talk",
ns("lets-talk"),
label = "Let's Talk",
class = "cta-button",
onclick = "window.open('https://appsilon.com/#contact', '_blank');"
Expand Down
1 change: 0 additions & 1 deletion app/view/mod_job_list.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
box::use(
magrittr[`%>%`],
reactable[
colDef,
getReactableState,
Expand Down
Loading
Loading