AI API Integration

Three core LLM pipeline patterns: batch-processing files, caching responses to avoid redundant API calls, and routing prompts to different models by task type.

All scripts use the OpenAI Chat Completions format, which is also compatible with Ollama and most self-hosted models. Pass your API key as the first argument.

Back to Kookbook


LLM Prompt Pipeline

Modules: curl plugin · JSON · CSV · File · Path · OS · List · Time · Konsol

Reads every .txt file from an input directory, sends each one to gpt-4o-mini, and writes the results to results.csv - one row per file with filename, a prompt snippet, the LLM response, and elapsed milliseconds.

Key patterns:

Ships with three sample prompts: prompt1.txt, prompt2.txt, prompt3.txt.

Usage

minks llm_prompt_pipeline.ks <api_key> <input_dir>

# example using the bundled samples
minks llm_prompt_pipeline.ks sk-... kookbook/ai-api-integration

Sample output

=== LLM Prompt Pipeline ===
Found 3 .txt file(s) in kookbook/ai-api-integration
[1/3] prompt1.txt
  843ms - Serverless architecture eliminates server management overhead...
[2/3] prompt2.txt
  612ms - def reverse_string(s): ...
[3/3] prompt3.txt
  534ms - 1. Use HTTPS everywhere...
Processed 3 file(s) - results.csv written.
Done.

Script

// llm_prompt_pipeline.ks - batch-process text files through an LLM, collect to CSV
// Modules: curl plugin, JSON, CSV, File, Path, OS, List, Time, Konsol
// Usage:  minks llm_prompt_pipeline.ks <api_key> <input_dir>
//
// Reads every .txt file in <input_dir>, sends each one as a prompt to the
// OpenAI Chat Completions API, and writes results to results.csv:
//   filename, prompt_snippet, response, elapsed_ms
//
// Sample input files ship alongside this script: prompt1.txt, prompt2.txt, prompt3.txt

#include "curl"

Konsol:Print("=== LLM Prompt Pipeline ===");

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

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

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

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

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

// ── Step 2: List .txt files in the input directory ────────────────────────────
Var:List entries;
OS:ListDirectory(inputDir, entries);

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

// Collect only .txt files.
Var:List txtFiles;
for (Number i = 0; i < entryCount; i++) {
    Var:String name;
    List:Get(i, entries, name);

    Var:String ext;
    Path:Extension(name, ext);
    if (ext == ".txt") {
        Var:String fullPath;
        Path:Join(inputDir, name, fullPath);
        List:Push(fullPath, txtFiles);
    }
}

Var:Number fileCount;
List:Size(txtFiles, fileCount);
Konsol:Print("Found ${fileCount} .txt file(s) in ${inputDir}");

if (fileCount == 0) {
    Konsol:Print("No .txt files found - nothing to process.");
    Konsol:Exit(0);
}

// ── Step 3: Set up the output CSV ────────────────────────────────────────────
// CSV:Set(row, col, value, docName) - builds the document in memory.
// CSV:Stringify(docName, outStr) serializes it when done.
CSV:Set(0, 0, "filename", results);
CSV:Set(0, 1, "prompt_snippet", results);
CSV:Set(0, 2, "response", results);
CSV:Set(0, 3, "elapsed_ms", results);

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

// ── Step 5: Process each file ─────────────────────────────────────────────────
Var:Number csvRow = 1;

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

    // Extract the filename for display and CSV.
    Var:String fileName;
    Path:FileName(filePath, fileName);
    Var:Number num = fi + 1;
    Konsol:Print("[${num}/${fileCount}] ${fileName}");

    // Read the file content.
    Var:Number fh;
    File:Open(filePath, "r", fh);
    Var:String prompt = "";
    Var:String line;
    Var:Boolean eof;
    File:EOF(fh, eof);
    while (!eof) {
        File:ReadLine(fh, line);
        prompt = prompt + line + "\n";
        File:EOF(fh, eof);
    }
    File:Close(fh);

    String:Trim(prompt, prompt);

    Var:String safePrompt;
    String:Replace(prompt, "\"", "\\\"", safePrompt);

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

    // Time the API call.
    Var:Number t1;
    Time:GetTimer(t1);

    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 t2;
    Time:GetTimer(t2);

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

    if (curlErr != "" || status != 200) {
        Var:String errMsg = curlErr != "" ? curlErr : "HTTP ${status}";
        Konsol:Print("  API error ${errMsg} - skipping.");
        CSV:Set(csvRow, 0, fileName, results);
        CSV:Set(csvRow, 1, "ERROR", results);
        CSV:Set(csvRow, 2, "HTTP ${status}", results);
        CSV:Set(csvRow, 3, "0", results);
        csvRow = csvRow + 1;
    } else {
        // Extract the assistant reply from choices[0].message.content.
        JSON:Parse(body, resp);
        Var:String answer;
        JSON:Get("choices.0.message.content", resp, answer);

        // Compute elapsed milliseconds.
        Var:Number elapsedMs = (t2 - t1) * 1000;
        Math:Floor(elapsedMs, elapsedMs);

        // Truncate prompt to 60 chars for the snippet column.
        Var:Number plen;
        String:Length(prompt, plen);
        Var:String snippet = prompt;
        if (plen > 60) {
            String:Mid(prompt, 1, 60, snippet);
            snippet = snippet + "...";
        }

        Konsol:Print("  ${elapsedMs}ms - ${answer}");

        CSV:Set(csvRow, 0, fileName, results);
        CSV:Set(csvRow, 1, snippet, results);
        CSV:Set(csvRow, 2, answer, results);
        CSV:Set(csvRow, 3, "${elapsedMs}", results);
        csvRow = csvRow + 1;
    }
}

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

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

Var:Number processed = csvRow - 1;
Konsol:Print("Processed ${processed} file(s) - results.csv written.");
Konsol:Print("Done.");


Semantic Cache

Modules: curl plugin · Hash · sqlite plugin · JSON · String · Time · Konsol

Hashes each prompt with SHA-256 to produce a cache key, checks SQLite for a stored response, and only calls the LLM on a cache miss. Hit count is tracked per entry. The cache.db database persists between runs.

Key patterns:

Usage

minks semantic_cache.ks <api_key> "<prompt>"

# first call - cache miss, hits the API
minks semantic_cache.ks sk-... "What is a transformer model?"

# second call - cache hit, no API call
minks semantic_cache.ks sk-... "What is a transformer model?"

Sample output (cache miss → hit)

=== Semantic Cache ===
Prompt : What is a transformer model?
Key    : 9f86d081...
Cache MISS - calling API...
API responded in 721ms
Cached for future requests.

A transformer model is a neural network architecture...

---

Cache HIT (served 2 time(s))

A transformer model is a neural network architecture...

Script

// semantic_cache.ks - SHA-256 prompt cache backed by SQLite
// Modules: curl plugin, Hash, sqlite plugin, JSON, String, Konsol
// Usage:  minks semantic_cache.ks <api_key> "<prompt>"
//
// Hashes the prompt with SHA-256 to produce a cache key.
// On a cache hit the stored response is returned instantly - no API call.
// On a miss the LLM is queried and the response is stored for next time.
// The cache database (cache.db) persists between runs.

#include "curl"
#include "sqlite"

Konsol:Print("=== Semantic Cache ===");

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

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

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

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

// ── Step 2: Compute the cache key ─────────────────────────────────────────────
// SHA-256 of the exact prompt string - same prompt always hits the same key.
Var:String cacheKey;
Hash:SHA256(prompt, cacheKey);
Konsol:Print("Prompt : ${prompt}");
Konsol:Print("Key    : ${cacheKey}");

// ── Step 3: Open the cache database ──────────────────────────────────────────
Var:Number db;
SQLite:Open("cache.db", db);

Var:Boolean ok;
SQLite:Exec(db, "CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, prompt TEXT, response TEXT, hits INTEGER DEFAULT 0)", ok);

// ── Step 4: Check for a cache hit ─────────────────────────────────────────────
// SQLite:QueryOne returns a single JSON object, or "" if no row matched.
Var:String row;
SQLite:QueryOne(db, "SELECT response, hits FROM cache WHERE key = '${cacheKey}'", row);

if (row != "" && row != "null") {
    // Cache hit - no API call needed.
    JSON:Parse(row, hitDoc);
    Var:String cachedResponse;
    Var:Number hits;
    JSON:Get("response", hitDoc, cachedResponse);
    JSON:Get("hits", hitDoc, hits);

    hits = hits + 1;
    SQLite:Exec(db, "UPDATE cache SET hits = ${hits} WHERE key = '${cacheKey}'", ok);
    SQLite:Close(db);

    Konsol:Print("Cache HIT (served ${hits} time(s))");
    Konsol:Print("");
    Konsol:Print(cachedResponse);
    Konsol:Exit(0);
}

// ── Step 5: Cache miss - call the LLM API ────────────────────────────────────
Konsol:Print("Cache MISS - calling API...");

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

Var:String safePrompt;
String:Replace(prompt, "\"", "\\\"", safePrompt);

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

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

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}");
    SQLite:Close(db);
    Konsol:Exit(1);
}

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

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

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

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

Var:Number elapsedMs = (t2 - t1) * 1000;
Math:Floor(elapsedMs, elapsedMs);
Konsol:Print("API responded in ${elapsedMs}ms");

// ── Step 6: Store in cache ───────────────────────────────────────────────────
// Escape single quotes in the stored values before interpolating into SQL.
Var:String safeAnswer;
String:Replace(answer, "'", "''", safeAnswer);
String:Replace(prompt, "'", "''", safePrompt);

SQLite:Exec(db, "INSERT INTO cache (key, prompt, response, hits) VALUES ('${cacheKey}', '${safePrompt}', '${safeAnswer}', 1)", ok);
SQLite:Close(db);

Konsol:Print("Cached for future requests.");
Konsol:Print("");
Konsol:Print(answer);
Konsol:Print("Done.");


Multi-Model Router

Modules: curl plugin · JSON · Dictionary · Time · String · Konsol

Uses two Dictionary lookup tables - one for model names, one for endpoint URLs - to route a prompt to the right LLM based on task type. Adding a new provider means adding one entry to each Dictionary.

Task Model Endpoint
fast gpt-4o-mini OpenAI
quality gpt-4o OpenAI
local llama3 Ollama (localhost)

Key patterns:

Usage

minks multi_model_router.ks <api_key> <task> "<prompt>"

minks multi_model_router.ks sk-... fast    "What is a REST API?"
minks multi_model_router.ks sk-... quality "Write a sonnet about recursion."
minks multi_model_router.ks sk-... local   "Summarize this in one line."

Sample output

=== Multi-Model Router ===
Task     : fast
Model    : gpt-4o-mini
Endpoint : https://api.openai.com/v1/chat/completions
Sending prompt...
Latency  : 487ms

--- Response ---
A REST API is an architectural style for building web services...
Done.

Script

// multi_model_router.ks - route prompts to different models by task type
// Modules: curl plugin, JSON, Dictionary, Time, String, Konsol
// Usage:  minks multi_model_router.ks <openai_api_key> <task> "<prompt>"
//
// Tasks and their routes:
//   fast    → gpt-4o-mini  (OpenAI - low latency, lower cost)
//   quality → gpt-4o       (OpenAI - best quality)
//   local   → llama3       (Ollama at localhost:11434 - no API key needed)
//
// Extend by adding more entries to the 'models' and 'endpoints' Dictionaries.

#include "curl"

Konsol:Print("=== Multi-Model Router ===");

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

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

if (argc < 3) {
    Konsol:Print("Usage: minks multi_model_router.ks <api_key> <task> \"<prompt>\"");
    Konsol:Print("Tasks: fast | quality | local");
    Konsol:Exit(1);
}

Var:String apiKey;
Var:String task;
Var:String prompt;
List:Get(0, args, apiKey);
List:Get(1, args, task);
List:Get(2, args, prompt);

// ── Step 2: Build the routing table ──────────────────────────────────────────
// Two Dictionaries - one for model names, one for endpoint URLs.
// Adding a new provider means adding one entry to each Dictionary.
Dictionary:New models;
Dictionary:Set("fast",    "gpt-4o-mini", models);
Dictionary:Set("quality", "gpt-4o",      models);
Dictionary:Set("local",   "llama3",      models);

Dictionary:New endpoints;
Dictionary:Set("fast",    "https://api.openai.com/v1/chat/completions",   endpoints);
Dictionary:Set("quality", "https://api.openai.com/v1/chat/completions",   endpoints);
Dictionary:Set("local",   "http://localhost:11434/v1/chat/completions",   endpoints);

// ── Step 3: Look up the route ─────────────────────────────────────────────────
Var:Boolean hasRoute;
Dictionary:Has(task, models, hasRoute);
if (!hasRoute) {
    Konsol:Print("Unknown task '${task}' - choose: fast, quality, local");
    Konsol:Exit(1);
}

Var:String model;
Var:String endpoint;
Dictionary:Get(task, models, model);
Dictionary:Get(task, endpoints, endpoint);

Konsol:Print("Task     : ${task}");
Konsol:Print("Model    : ${model}");
Konsol:Print("Endpoint : ${endpoint}");

// ── Step 4: Set request headers ───────────────────────────────────────────────
// Ollama accepts any non-empty Authorization value; OpenAI needs a valid key.
if (task == "local") {
    Curl:SetHeader("Authorization", "Bearer ollama");
} else {
    Curl:SetHeader("Authorization", "Bearer ${apiKey}");
}
Curl:SetHeader("Content-Type", "application/json");
Curl:SetTimeout(60);

// ── Step 5: Build and send the request ───────────────────────────────────────
Var:String safePrompt;
String:Replace(prompt, "\"", "\\\"", safePrompt);

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

Konsol:Print("Sending prompt...");

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

Var:String body;
try {
    Curl:Post(endpoint, payload, body);
} catch (CurlException e) {
    Konsol:Print("API request failed: ${e.message}");
    Konsol:Exit(1);
}

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

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

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

// ── Step 6: Parse and display the response ────────────────────────────────────
// All three routes use OpenAI-compatible response format:
// choices[0].message.content holds the assistant reply.
JSON:Parse(body, resp);
Var:String answer;
JSON:Get("choices.0.message.content", resp, answer);

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

Konsol:Print("Latency  : ${elapsedMs}ms");
Konsol:Print("");
Konsol:Print("--- Response ---");
Konsol:Print(answer);
Konsol:Print("Done.");


Back to Kookbook