Skip to content

Commit

Permalink
Test with Deno 2
Browse files Browse the repository at this point in the history
  • Loading branch information
ariya committed Nov 27, 2024
1 parent c2703fc commit 0f8671d
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/test-deno.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Test with Deno

on: [push, pull_request, workflow_dispatch]

jobs:
test:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- run: deno --version

- name: Prepare LLM
uses: ./.github/actions/prepare-llm
timeout-minutes: 3

- run: echo 'Which planet in our solar system is the largest?' | ./ask-llm.ts | grep -i jupiter
timeout-minutes: 7
env:
LLM_API_BASE_URL: 'http://127.0.0.1:8080/v1'
LLM_DEBUG: 1
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ It is available in several flavors:

* Python version. Compatible with [CPython](https://python.org) or [PyPy](https://pypy.org), v3.10 or higher.
* JavaScript version. Compatible with [Node.js](https://nodejs.org) (>= v18) or [Bun](https://bun.sh) (>= v1.0).
* TypeScript version. Compatible with [Deno](https://deno.com/) (>= 2.0) or [Bun](https://bun.sh) (>= v1.0).
* Clojure version. Compatible with [Babashka](https://babashka.org/) (>= 1.3).
* Swift version. Compatible with [Swift](https://www.swift.org), v5.10 or higher.
* Go version. Compatible with [Go](https://golang.org), v1.19 or higher.
Expand All @@ -18,6 +19,7 @@ Interact with the LLM with:
```bash
./ask-llm.py # for Python user
./ask-llm.js # for Node.js user
./ask-llm.ts # for Deno user
./ask-llm.clj # for Clojure user
./ask-llm.swift # for Swift user
go run ask-llm.go # for Go user
Expand Down
152 changes: 152 additions & 0 deletions ask-llm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env -S deno run --allow-env --allow-net

import readline from 'node:readline';

const LLM_API_BASE_URL = process.env.LLM_API_BASE_URL || 'https://api.openai.com/v1';
const LLM_API_KEY = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY;
const LLM_CHAT_MODEL = process.env.LLM_CHAT_MODEL;
const LLM_STREAMING = process.env.LLM_STREAMING !== 'no';

const LLM_DEBUG = process.env.LLM_DEBUG;

/**
* Represents a chat message.
*
* @typedef {Object} Message
* @property {'system'|'user'|'assistant'} role
* @property {string} content
*/

/**
* A callback function to stream then completion.
*
* @callback CompletionHandler
* @param {string} text
* @returns {void}
*/

/**
* Generates a chat completion using a RESTful LLM API service.
*
* @param {Array<Message>} messages - List of chat messages.
* @param {CompletionHandler=} handler - An optional callback to stream the completion.
* @returns {Promise<string>} The completion generated by the LLM.
*/
const chat = async (messages, handler) => {
const url = `${LLM_API_BASE_URL}/chat/completions`;
const auth = LLM_API_KEY ? { 'Authorization': `Bearer ${LLM_API_KEY}` } : {};
const model = LLM_CHAT_MODEL || 'gpt-4o-mini';
const stop = ['<|im_end|>', '<|end|>', '<|eot_id|>'];
const max_tokens = 200;
const temperature = 0;
const stream = LLM_STREAMING && typeof handler === 'function';
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...auth },
body: JSON.stringify({ messages, model, stop, max_tokens, temperature, stream })
});
if (!response.ok) {
throw new Error(`HTTP error with the status: ${response.status} ${response.statusText}`);
}

if (!stream) {
const data = await response.json();
const { choices } = data;
const first = choices[0];
const { message } = first;
const { content } = message;
const answer = content.trim();
handler && handler(answer);
return answer;
}

const parse = (line) => {
let partial = null;
const prefix = line.substring(0, 6);
if (prefix === 'data: ') {
const payload = line.substring(6);
try {
const { choices } = JSON.parse(payload);
const [choice] = choices;
const { delta } = choice;
partial = delta?.content;
} catch (e) {
// ignore
} finally {
return partial;
}
}
return partial;
}

const reader = response.body.getReader();
const decoder = new TextDecoder();

let answer = '';
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
const lines = decoder.decode(value).split('\n');
for (let i = 0; i < lines.length; ++i) {
const line = buffer + lines[i];
if (line[0] === ':') {
buffer = '';
continue;
}
if (line === 'data: [DONE]') {
break;
}
if (line.length > 0) {
const partial = parse(line.trim());
if (partial === null) {
buffer = line;
} else if (partial && partial.length > 0) {
buffer = '';
if (answer.length < 1) {
const leading = partial.trim();
answer = leading;
handler && (leading.length > 0) && handler(leading);
} else {
answer += partial;
handler && handler(partial);
}
}
}
}
}
return answer;
}

const SYSTEM_PROMPT = 'Answer the question politely and concisely.';

(async () => {
console.log(`Using LLM at ${LLM_API_BASE_URL}.`);
console.log('Press Ctrl+D to exit.')
console.log();

const messages = [];
messages.push({ role: 'system', content: SYSTEM_PROMPT });

let loop = true;
const io = readline.createInterface({ input: process.stdin, output: process.stdout });
io.on('close', () => { loop = false; });

const qa = () => {
io.question('>> ', async (question) => {
messages.push({ role: 'user', content: question });
const start = Date.now();
const answer = await chat(messages, (str) => process.stdout.write(str));
messages.push({ role: 'assistant', content: answer.trim() });
console.log();
const elapsed = Date.now() - start;
LLM_DEBUG && console.log(`[${elapsed} ms]`);
console.log();
loop && qa();
})
}

qa();
})();

0 comments on commit 0f8671d

Please sign in to comment.