DevOps + AI

Three recipes for AI-assisted operations: triage server logs, benchmark LLM endpoints, and automate code review on KonsolScript source files.

Back to Kookbook


AI Log Triage

Modules: curl plugin · File · Regex · String · List · JSON · Time · Konsol

Scans a structured log file, extracts every [ERROR] line with Regex:Groups, batches them into a single LLM prompt asking for root-cause analysis, and writes a Markdown report with the findings.

Key patterns:

Ships with server.log (15 realistic log entries including 5 errors).

Usage

minks log_triage.ks <api_key> <logfile> [report.md]

minks log_triage.ks sk-... server.log
minks log_triage.ks sk-... server.log incident_report.md

Sample output

=== AI Log Triage ===
Log summary - INFO: 8  WARN: 2  ERROR: 5
Sending 5 error(s) to LLM for analysis...
Report written to triage_report.md

## Root Causes
- Database connection pool exhaustion caused cascading timeouts...
- Disk space exhaustion on /var/data disabled the audit log subsystem...

## Recommended Actions
- Increase connection pool size or add read replicas...
Done.

Script

// log_triage.ks - scan a log file with Regex, send errors to LLM for analysis
// Modules: curl plugin, File, Regex, String, List, JSON, Konsol
// Usage:  minks log_triage.ks <api_key> <logfile> [report.md]
//
// Reads a structured log, extracts every ERROR line with Regex, batches them
// into a single prompt, and asks the LLM to identify root causes and suggest
// fixes.  Writes the analysis to a Markdown report.
//
// Expected log format:  YYYY-MM-DD HH:MM:SS [LEVEL] message
// Sample log ships alongside this script: server.log

#include "curl"

Konsol:Print("=== AI Log Triage ===");

// ── Step 1: Read arguments ────────────────────────────────────────────────────
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 2) {
    Konsol:Print("Usage: minks log_triage.ks <api_key> <logfile> [report.md]");
    Konsol:Exit(1);
}

Var:String apiKey;
Var:String logPath;
List:Get(0, args, apiKey);
List:Get(1, args, logPath);

Var:String reportPath = "triage_report.md";
if (argc >= 3) {
    List:Get(2, args, reportPath);
}

// ── Step 2: Scan the log and collect ERROR lines ──────────────────────────────
Var:Number fh;
File:Open(logPath, "r", fh);

Var:Number infoCount = 0;
Var:Number warnCount = 0;
Var:Number errCount = 0;
Var:List errorLines;

Var:String line;
Var:Boolean eof;
Var:List groups;
Var:Number gc;

File:EOF(fh, eof);
while (!eof) {
    File:ReadLine(fh, line);
    String:Trim(line, line);

    if (line != "") {
        List:Clear(groups);
        Regex:Groups("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[(INFO|WARN|ERROR)\\]\\s*(.+)$", line, groups);
        List:Size(groups, gc);

        if (gc >= 4) {
            Var:String level;
            Var:String message;
            List:Get(2, groups, level);
            List:Get(3, groups, message);

            if (level == "INFO")  { infoCount  = infoCount  + 1; }
            if (level == "WARN")  { warnCount  = warnCount  + 1; }
            if (level == "ERROR") {
                errCount = errCount + 1;
                // Store timestamp + message for the LLM prompt.
                Var:String ts;
                List:Get(1, groups, ts);
                List:Push("[${ts}] ${message}", errorLines);
            }
        }
    }

    File:EOF(fh, eof);
}
File:Close(fh);

Konsol:Print("Log summary - INFO: ${infoCount}  WARN: ${warnCount}  ERROR: ${errCount}");

if (errCount == 0) {
    Konsol:Print("No errors found - nothing to triage.");
    Konsol:Exit(0);
}

// ── Step 3: Build the LLM prompt ─────────────────────────────────────────────
// Concatenate all error lines into a single block for the LLM to analyse.
Var:String errorBlock = "";
for (Number ei = 0; ei < errCount; ei++) {
    Var:String errLine;
    List:Get(ei, errorLines, errLine);
    errorBlock = errorBlock + errLine + "\n";
}

Var:String sysMsg = "You are a DevOps engineer. Given a list of server error log entries, identify the most likely root causes and provide concise remediation steps. Format your response as Markdown with sections: ## Root Causes and ## Recommended Actions.";
Var:String userMsg = "Here are the error entries:\n\n" + errorBlock;

Var:String safeUserMsg;
String:Replace(userMsg, "\"", "\\\"", safeUserMsg);

Var:String payload = """{
  "model": "gpt-4o-mini",
  "messages": [
    {"role": "system", "content": "${sysMsg}"},
    {"role": "user", "content": "${safeUserMsg}"}
  ],
  "max_tokens": 800
}""";

// ── Step 4: Call the LLM ──────────────────────────────────────────────────────
Konsol:Print("Sending ${errCount} error(s) to LLM for analysis...");

Curl:SetHeader("Authorization", "Bearer ${apiKey}");
Curl:SetHeader("Content-Type", "application/json");
Curl:SetTimeout(45);

Var:String body;
try {
    Curl:Post("https://api.openai.com/v1/chat/completions", payload, body);
} catch (CurlException e) {
    Konsol:Print("API request failed: ${e.message}");
    Konsol:Exit(1);
}

Var:Number status;
Curl:Status(status);

if (status != 200) {
    Konsol:Print("API error ${status}: ${body}");
    Konsol:Exit(1);
}

JSON:Parse(body, resp);
Var:String analysis;
JSON:Get("choices.0.message.content", resp, analysis);

// ── Step 5: Write the Markdown report ────────────────────────────────────────
Var:Number yr;
Var:Number mo;
Var:Number dy;
Time:GetYear(yr);
Time:GetMonth(mo);
Time:GetDay(dy);

Var:Number wh;
File:Open(reportPath, "w", wh);
File:Write("# Log Triage Report\n\n", wh);
File:Write("**Log file:** ${logPath}\n", wh);
File:Write("**Generated:** ${yr}-${mo}-${dy}\n\n", wh);
File:Write("## Log Summary\n\n", wh);
File:Write("| Level | Count |\n|-------|-------|\n", wh);
File:Write("| INFO  | ${infoCount} |\n", wh);
File:Write("| WARN  | ${warnCount} |\n", wh);
File:Write("| ERROR | ${errCount} |\n\n", wh);
File:Write("## Error Entries\n\n```\n${errorBlock}```\n\n", wh);
File:Write("## LLM Analysis\n\n", wh);
File:Write(analysis, wh);
File:Write("\n", wh);
File:Close(wh);

Konsol:Print("Report written to ${reportPath}");
Konsol:Print(analysis);
Konsol:Print("Done.");


AI Model Benchmarker

Modules: curl plugin · JSON · CSV · Time · String · File · Konsol

Sends an identical prompt to each configured model/endpoint, measures round-trip latency with Time:GetTimer, records prompt and completion token counts, and writes a ranked benchmark_results.csv.

Key patterns:

Usage

minks model_benchmarker.ks <openai_api_key>

minks model_benchmarker.ks sk-...

Sample output

=== LLM Model Benchmarker ===
Prompt: In exactly two sentences, explain what a REST API is.

Testing: OpenAI gpt-4o-mini
  487ms  12+42 tokens
  A REST API is an architectural style for networked applications...

Testing: OpenAI gpt-4o
  1203ms  12+38 tokens
  REST APIs use HTTP methods to enable communication between systems...

Testing: Ollama llama3 (local)
  HTTP 500 - skipped.

Results saved to benchmark_results.csv
Done.

Script

// model_benchmarker.ks - benchmark multiple LLM endpoints on latency and output
// Modules: curl plugin, JSON, CSV, Time, String, File, Konsol
// Usage:  minks model_benchmarker.ks <openai_api_key>
//
// Sends an identical prompt to each configured model/endpoint, measures
// round-trip latency, records token counts, and writes a ranked CSV summary.
// Add or remove entries from the 'suite' List to change what gets benchmarked.

#include "curl"

Konsol:Print("=== LLM Model Benchmarker ===");

// ── Step 1: Read API key ──────────────────────────────────────────────────────
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 1) {
    Konsol:Print("Usage: minks model_benchmarker.ks <openai_api_key>");
    Konsol:Exit(1);
}

Var:String apiKey;
List:Get(0, args, apiKey);

// ── Step 2: Define the benchmark suite ───────────────────────────────────────
// Each entry is a "label|model|endpoint|auth" record stored in a List.
// Pipe-delimited so we can split it back out with Regex:Groups.
Var:List suite;
List:Push("OpenAI gpt-4o-mini|gpt-4o-mini|https://api.openai.com/v1/chat/completions|openai", suite);
List:Push("OpenAI gpt-4o|gpt-4o|https://api.openai.com/v1/chat/completions|openai", suite);
List:Push("Ollama llama3 (local)|llama3|http://localhost:11434/v1/chat/completions|local", suite);

Var:Number suiteCount;
List:Size(suite, suiteCount);

// ── Step 3: The benchmark prompt ─────────────────────────────────────────────
// Use a fixed prompt so all models answer the same question.
Var:String benchPrompt = "In exactly two sentences, explain what a REST API is.";
Var:String safePrompt;
String:Replace(benchPrompt, "\"", "\\\"", safePrompt);

Konsol:Print("Prompt: ${benchPrompt}");
Konsol:Print("");

// ── Step 4: Initialise the output CSV ────────────────────────────────────────
CSV:Set(0, 0, "model", results);
CSV:Set(0, 1, "latency_ms", results);
CSV:Set(0, 2, "prompt_tokens", results);
CSV:Set(0, 3, "completion_tokens", results);
CSV:Set(0, 4, "response_snippet", results);

// ── Step 5: Run each benchmark ────────────────────────────────────────────────
Var:List groups;
Var:Number gc;

for (Number i = 0; i < suiteCount; i++) {
    Var:String entry;
    List:Get(i, suite, entry);

    // Parse the pipe-delimited record.
    List:Clear(groups);
    Regex:Groups("^([^|]+)\\|([^|]+)\\|([^|]+)\\|([^|]+)$", entry, groups);
    List:Size(groups, gc);
    if (gc < 5) {
        Konsol:Print("Malformed suite entry - skipping.");
    } else {
        Var:String label;
        Var:String model;
        Var:String endpoint;
        Var:String auth;
        List:Get(1, groups, label);
        List:Get(2, groups, model);
        List:Get(3, groups, endpoint);
        List:Get(4, groups, auth);

        Konsol:Print("Testing: ${label}");

        // Set headers for this provider.
        Curl:ClearHeaders();
        Curl:SetHeader("Content-Type", "application/json");
        if (auth == "openai") {
            Curl:SetHeader("Authorization", "Bearer ${apiKey}");
        } else {
            Curl:SetHeader("Authorization", "Bearer ollama");
        }
        Curl:SetTimeout(60);

        Var:String payload = """{
          "model": "${model}",
          "messages": [{"role": "user", "content": "${safePrompt}"}],
          "max_tokens": 100
        }""";

        Var:Number t1;
        Time:GetTimer(t1);

        Var:String body;
        Var:String curlErr = "";
        try {
            Curl:Post(endpoint, payload, body);
        } catch (CurlException e) {
            curlErr = e.message;
        }

        Var:Number t2;
        Time:GetTimer(t2);

        Var:Number status;
        Curl:Status(status);

        Var:Number csvRow = i + 1;

        if (curlErr != "" || status != 200) {
            Var:String errMsg = curlErr != "" ? curlErr : "HTTP ${status}";
            Konsol:Print("  ${errMsg} - skipped.");
            CSV:Set(csvRow, 0, label, results);
            CSV:Set(csvRow, 1, "ERROR", results);
            CSV:Set(csvRow, 2, "0", results);
            CSV:Set(csvRow, 3, "0", results);
            CSV:Set(csvRow, 4, errMsg, results);
        } else {
            JSON:Parse(body, resp);
            Var:String answer;
            Var:Number promptTok;
            Var:Number completionTok;
            JSON:Get("choices.0.message.content", resp, answer);
            JSON:Get("usage.prompt_tokens", resp, promptTok);
            JSON:Get("usage.completion_tokens", resp, completionTok);

            Var:Number elapsedMs = (t2 - t1) * 1000;
            Math:Floor(elapsedMs, elapsedMs);

            // Truncate response to 80 chars for the snippet column.
            Var:Number alen;
            String:Length(answer, alen);
            Var:String snippet = answer;
            if (alen > 80) {
                String:Mid(answer, 1, 80, snippet);
                snippet = snippet + "...";
            }

            Konsol:Print("  ${elapsedMs}ms  ${promptTok}+${completionTok} tokens");
            Konsol:Print("  ${answer}");
            Konsol:Print("");

            CSV:Set(csvRow, 0, label, results);
            CSV:Set(csvRow, 1, "${elapsedMs}", results);
            CSV:Set(csvRow, 2, "${promptTok}", results);
            CSV:Set(csvRow, 3, "${completionTok}", results);
            CSV:Set(csvRow, 4, snippet, results);
        }
    }
}

// ── Step 6: Save results.csv ──────────────────────────────────────────────────
Var:String csvText;
CSV:Stringify(results, csvText);
CSV:Free(results);

Var:Number wh;
File:Open("benchmark_results.csv", "w", wh);
File:Write(csvText, wh);
File:Close(wh);

Konsol:Print("Results saved to benchmark_results.csv");
Konsol:Print("Done.");


Automated Code Reviewer

Modules: curl plugin · OS · File · Path · JSON · String · List · Konsol

Walks a source directory for .ks files, sends each to gpt-4o-mini with a KonsolScript-aware review prompt, and writes the feedback to a <filename>.review file alongside the source.

Key patterns:

Usage

minks code_reviewer.ks <api_key> <source_dir>

minks code_reviewer.ks sk-... ./src
minks code_reviewer.ks sk-... kookbook/cli-tools-and-automation

Sample output

=== Automated Code Reviewer ===
Found 3 .ks file(s) to review.
[1/3] Reviewing file_organizer.ks...
  → kookbook/cli-tools-and-automation/file_organizer.ks.review
[2/3] Reviewing build_runner.ks...
  → kookbook/cli-tools-and-automation/build_runner.ks.review
[3/3] Reviewing log_analyzer.ks...
  → kookbook/cli-tools-and-automation/log_analyzer.ks.review
Reviewed 3 file(s).
Done.

Script

// code_reviewer.ks - send .ks source files to an LLM for automated code review
// Modules: curl plugin, OS, File, Path, JSON, String, List, Konsol
// Usage:  minks code_reviewer.ks <api_key> <source_dir>
//
// Walks <source_dir> for .ks files, sends each one to the LLM with a code
// review prompt, and writes the feedback to <filename>.review alongside the
// source file.  Useful as a pre-commit check or post-change audit.

#include "curl"

Konsol:Print("=== Automated Code Reviewer ===");

// ── Step 1: Read arguments ────────────────────────────────────────────────────
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 2) {
    Konsol:Print("Usage: minks code_reviewer.ks <api_key> <source_dir>");
    Konsol:Exit(1);
}

Var:String apiKey;
Var:String srcDir;
List:Get(0, args, apiKey);
List:Get(1, args, srcDir);

Var:Boolean dirOk;
Path:IsDirectory(srcDir, dirOk);
if (!dirOk) {
    Konsol:Print("Not a directory: ${srcDir}");
    Konsol:Exit(1);
}

// ── Step 2: Collect .ks files ─────────────────────────────────────────────────
Var:List entries;
OS:ListDirectory(srcDir, entries);

Var:Number entryCount;
List:Size(entries, entryCount);

Var:List ksFiles;
for (Number i = 0; i < entryCount; i++) {
    Var:String name;
    List:Get(i, entries, name);
    Var:String ext;
    Path:Extension(name, ext);
    if (ext == ".ks") {
        Var:String fp;
        Path:Join(srcDir, name, fp);
        List:Push(fp, ksFiles);
    }
}

Var:Number fileCount;
List:Size(ksFiles, fileCount);
Konsol:Print("Found ${fileCount} .ks file(s) to review.");

if (fileCount == 0) {
    Konsol:Print("Nothing to review.");
    Konsol:Exit(0);
}

// ── Step 3: Set shared headers ────────────────────────────────────────────────
Curl:SetHeader("Authorization", "Bearer ${apiKey}");
Curl:SetHeader("Content-Type", "application/json");
Curl:SetTimeout(45);

Var:String sysMsg = "You are an expert KonsolScript code reviewer. Review the provided script for: correctness, proper use of ByRef convention (output always in last arg), off-by-one errors, resource leaks (unclosed files/handles), and clarity. Be concise. Format as Markdown with sections: ## Summary, ## Issues, ## Suggestions.";

// ── Step 4: Review each file ──────────────────────────────────────────────────
Var:Number reviewed = 0;

for (Number fi = 0; fi < fileCount; fi++) {
    Var:String filePath;
    List:Get(fi, ksFiles, filePath);

    Var:String fileName;
    Path:FileName(filePath, fileName);
    Var:Number num = fi + 1;
    Konsol:Print("[${num}/${fileCount}] Reviewing ${fileName}...");

    // Read the source file.
    Var:Number rfh;
    File:Open(filePath, "r", rfh);
    Var:String code = "";
    Var:String rline;
    Var:Boolean reof;
    File:EOF(rfh, reof);
    while (!reof) {
        File:ReadLine(rfh, rline);
        code = code + rline + "\n";
        File:EOF(rfh, reof);
    }
    File:Close(rfh);

    Var:String userMsg = "Review this KonsolScript file (${fileName}):\n\n```\n" + code + "\n```";
    Var:String safeUserMsg;
    String:Replace(userMsg, "\"", "\\\"", safeUserMsg);

    Var:String payload = """{
      "model": "gpt-4o-mini",
      "messages": [
        {"role": "system", "content": "${sysMsg}"},
        {"role": "user", "content": "${safeUserMsg}"}
      ],
      "max_tokens": 600
    }""";

    Var:String body;
    Var:String curlErr = "";
    try {
        Curl:Post("https://api.openai.com/v1/chat/completions", payload, body);
    } catch (CurlException e) {
        curlErr = e.message;
    }

    Var:Number status;
    Curl:Status(status);

    if (curlErr != "" || status != 200) {
        Var:String errMsg = curlErr != "" ? curlErr : "HTTP ${status}";
        Konsol:Print("  API error ${errMsg} - skipping.");
    } else {
        JSON:Parse(body, resp);
        Var:String review;
        JSON:Get("choices.0.message.content", resp, review);

        // Write review to <filename>.review in the same directory.
        Var:String reviewPath = filePath + ".review";
        Var:Number wh;
        File:Open(reviewPath, "w", wh);
        File:Write("# Code Review: ${fileName}\n\n", wh);
        File:Write(review, wh);
        File:Write("\n", wh);
        File:Close(wh);

        Konsol:Print("  → ${reviewPath}");
        reviewed = reviewed + 1;
    }
}

Konsol:Print("Reviewed ${reviewed} file(s).");
Konsol:Print("Done.");


Back to Kookbook