Consolidate Scheme and Common Lisp into single uncloseai files with environment-based discovery
This commit is contained in:
parent
c67d1b13da
commit
dad7943c54
14 changed files with 574 additions and 472 deletions
|
|
@ -15,11 +15,7 @@ RUN sbcl --eval '(ql:quickload :dexador)' \
|
|||
WORKDIR /app
|
||||
|
||||
# Copy application files
|
||||
COPY hermes-nonstreaming.lisp .
|
||||
COPY hermes-streaming.lisp .
|
||||
COPY qwen-nonstreaming.lisp .
|
||||
COPY qwen-streaming.lisp .
|
||||
COPY tts.lisp .
|
||||
COPY uncloseai.lisp .
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sbcl", "--eval", "(format t \"Available examples:~% sbcl --script hermes-nonstreaming.lisp~% sbcl --script hermes-streaming.lisp~% sbcl --script qwen-nonstreaming.lisp~% sbcl --script qwen-streaming.lisp~% sbcl --script tts.lisp~%\")", "--quit"]
|
||||
# Default command runs the demo
|
||||
CMD ["sbcl", "--script", "uncloseai.lisp"]
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Hermes AI Non-Streaming Example
|
||||
;;; Uses Dexador HTTP client and Jonathan JSON library
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
(defun make-chat-request (prompt)
|
||||
"Make a non-streaming chat completion request"
|
||||
(let* ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| :false)))
|
||||
(response (dex:post *base-url*
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload)))
|
||||
(let* ((parsed (jonathan:parse response))
|
||||
(choice (aref (getf parsed :|choices|) 0))
|
||||
(message (getf choice :|message|))
|
||||
(content (getf message :|content|)))
|
||||
content)))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Requesting from Hermes AI...~%~%")
|
||||
(let ((result (make-chat-request "Give a Python Fizzbuzz solution in one line of code?")))
|
||||
(format t "Response: ~A~%" result)))
|
||||
(error (e)
|
||||
(format t "Error: ~A~%" e)))
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Hermes AI Streaming Example
|
||||
;;; Uses Dexador with :want-stream for SSE streaming
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
(defun process-sse-line (line)
|
||||
"Process a single Server-Sent Event line"
|
||||
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
|
||||
(let ((data (subseq line 6)))
|
||||
(unless (string= data "[DONE]")
|
||||
(handler-case
|
||||
(let* ((parsed (jonathan:parse data))
|
||||
(choices (getf parsed :|choices|))
|
||||
(delta (when (> (length choices) 0)
|
||||
(getf (aref choices 0) :|delta|)))
|
||||
(content (when delta (getf delta :|content|))))
|
||||
(when content
|
||||
(format t "~A" content)
|
||||
(force-output)))
|
||||
(error (e) nil))))))
|
||||
|
||||
(defun make-streaming-request (prompt)
|
||||
"Make a streaming chat completion request"
|
||||
(let ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| t))))
|
||||
(dex:request *base-url*
|
||||
:method :post
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload
|
||||
:want-stream t
|
||||
:stream-callback
|
||||
(lambda (stream)
|
||||
(loop for line = (read-line stream nil nil)
|
||||
while line
|
||||
do (process-sse-line line))))))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Streaming from Hermes AI...~%~%")
|
||||
(make-streaming-request "Give a Python Fizzbuzz solution in one line of code?")
|
||||
(format t "~%~%Done!~%"))
|
||||
(error (e)
|
||||
(format t "~%Error: ~A~%" e)))
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Qwen 3 Coder Non-Streaming Example
|
||||
;;; Uses Dexador HTTP client and Jonathan JSON library
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(defun make-chat-request (prompt)
|
||||
"Make a non-streaming chat completion request"
|
||||
(let* ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| :false)))
|
||||
(response (dex:post *base-url*
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload)))
|
||||
(let* ((parsed (jonathan:parse response))
|
||||
(choice (aref (getf parsed :|choices|) 0))
|
||||
(message (getf choice :|message|))
|
||||
(content (getf message :|content|)))
|
||||
content)))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Requesting from Qwen 3 Coder...~%~%")
|
||||
(let ((result (make-chat-request "Give a Python Fizzbuzz solution in one line of code?")))
|
||||
(format t "Response: ~A~%" result)))
|
||||
(error (e)
|
||||
(format t "Error: ~A~%" e)))
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Qwen 3 Coder Streaming Example
|
||||
;;; Uses Dexador with :want-stream for SSE streaming
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(defparameter *model* "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(defun process-sse-line (line)
|
||||
"Process a single Server-Sent Event line"
|
||||
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
|
||||
(let ((data (subseq line 6)))
|
||||
(unless (string= data "[DONE]")
|
||||
(handler-case
|
||||
(let* ((parsed (jonathan:parse data))
|
||||
(choices (getf parsed :|choices|))
|
||||
(delta (when (> (length choices) 0)
|
||||
(getf (aref choices 0) :|delta|)))
|
||||
(content (when delta (getf delta :|content|))))
|
||||
(when content
|
||||
(format t "~A" content)
|
||||
(force-output)))
|
||||
(error (e) nil))))))
|
||||
|
||||
(defun make-streaming-request (prompt)
|
||||
"Make a streaming chat completion request"
|
||||
(let ((payload (jonathan:to-json
|
||||
(list :|model| *model*
|
||||
:|messages| (vector (list :|role| "user"
|
||||
:|content| prompt))
|
||||
:|temperature| 0.5
|
||||
:|max_tokens| 150
|
||||
:|stream| t))))
|
||||
(dex:request *base-url*
|
||||
:method :post
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload
|
||||
:want-stream t
|
||||
:stream-callback
|
||||
(lambda (stream)
|
||||
(loop for line = (read-line stream nil nil)
|
||||
while line
|
||||
do (process-sse-line line))))))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Streaming from Qwen 3 Coder...~%~%")
|
||||
(make-streaming-request "Give a Python Fizzbuzz solution in one line of code?")
|
||||
(format t "~%~%Done!~%"))
|
||||
(error (e)
|
||||
(format t "~%Error: ~A~%" e)))
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; Text-to-Speech Example
|
||||
;;; Uses Dexador to download audio file from TTS endpoint
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
(defparameter *base-url* "https://speech.ai.unturf.com/v1/audio/speech")
|
||||
|
||||
(defun generate-speech (text output-file)
|
||||
"Generate speech from text and save to file"
|
||||
(let ((payload (jonathan:to-json
|
||||
(list :|model| "tts-1"
|
||||
:|voice| "alloy"
|
||||
:|speed| 0.9
|
||||
:|input| text))))
|
||||
(with-open-file (out output-file
|
||||
:direction :output
|
||||
:if-exists :supersede
|
||||
:if-does-not-exist :create
|
||||
:element-type '(unsigned-byte 8))
|
||||
(let ((response (dex:post *base-url*
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:content payload
|
||||
:force-binary t)))
|
||||
(write-sequence response out)))
|
||||
(format t "Speech saved to: ~A~%" output-file)))
|
||||
|
||||
;; Main execution
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Generating speech from TTS...~%~%")
|
||||
(generate-speech
|
||||
"I think so therefore, Today is a wonderful day to grow something people love!"
|
||||
"speech.mp3"))
|
||||
(error (e)
|
||||
(format t "Error: ~A~%" e)))
|
||||
302
public/languages/commonlisp/uncloseai.lisp
Normal file
302
public/languages/commonlisp/uncloseai.lisp
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env sbcl --script
|
||||
|
||||
;;; uncloseai - Common Lisp client library for OpenAI-compatible APIs
|
||||
;;; Supports streaming and non-streaming chat, model discovery, and TTS
|
||||
;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
;;;
|
||||
;;; Uses Dexador HTTP client and Jonathan JSON library
|
||||
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:quickload '(:dexador :jonathan) :silent t)
|
||||
|
||||
;;; Data structures
|
||||
|
||||
(defclass uncloseai ()
|
||||
((models :initform nil :accessor models
|
||||
:documentation "List of discovered models with metadata")
|
||||
(tts-endpoints :initform nil :accessor tts-endpoints
|
||||
:documentation "List of TTS endpoint URLs")
|
||||
(api-key :initform nil :initarg :api-key :accessor api-key
|
||||
:documentation "Optional API key for authentication")
|
||||
(timeout :initform 30 :initarg :timeout :accessor timeout
|
||||
:documentation "Request timeout in seconds")))
|
||||
|
||||
(defun make-model-info (id endpoint max-tokens)
|
||||
"Create a model info plist"
|
||||
(list :id id :endpoint endpoint :max-tokens max-tokens))
|
||||
|
||||
;;; Environment discovery
|
||||
|
||||
(defun discover-env-endpoints (prefix)
|
||||
"Discover endpoints from environment variables like PREFIX_1, PREFIX_2, ..."
|
||||
(loop for i from 1 to 9999
|
||||
for var-name = (format nil "~A_~D" prefix i)
|
||||
for endpoint = (uiop:getenv var-name)
|
||||
while endpoint
|
||||
collect endpoint))
|
||||
|
||||
;;; Model discovery
|
||||
|
||||
(defun discover-models-from-endpoint (client endpoint)
|
||||
"Discover available models from an endpoint"
|
||||
(handler-case
|
||||
(let* ((url (concatenate 'string endpoint "/models"))
|
||||
(headers (when (api-key client)
|
||||
(list (cons "Authorization"
|
||||
(format nil "Bearer ~A" (api-key client))))))
|
||||
(response (dex:get url :headers headers))
|
||||
(parsed (jonathan:parse response))
|
||||
(data (getf parsed :|data|)))
|
||||
(loop for model across data
|
||||
for model-id = (getf model :|id|)
|
||||
for max-tokens = (or (getf model :|max_model_len|) 8192)
|
||||
do (push (make-model-info model-id endpoint max-tokens)
|
||||
(models client))))
|
||||
(error (e)
|
||||
(format t "Warning: Failed to discover models from ~A: ~A~%" endpoint e))))
|
||||
|
||||
(defun initialize-client (client model-endpoints tts-endpoints)
|
||||
"Initialize client with endpoint discovery and model detection"
|
||||
;; Discover endpoints from environment if not provided
|
||||
(let ((model-eps (or model-endpoints (discover-env-endpoints "MODEL_ENDPOINT")))
|
||||
(tts-eps (or tts-endpoints (discover-env-endpoints "TTS_ENDPOINT"))))
|
||||
|
||||
;; Discover models from each endpoint
|
||||
(dolist (endpoint model-eps)
|
||||
(discover-models-from-endpoint client endpoint))
|
||||
|
||||
;; Reverse models list (they were pushed in reverse order)
|
||||
(setf (models client) (nreverse (models client)))
|
||||
|
||||
;; Store TTS endpoints
|
||||
(setf (tts-endpoints client) tts-eps))
|
||||
|
||||
client)
|
||||
|
||||
(defun make-uncloseai (&key model-endpoints tts-endpoints api-key (timeout 30))
|
||||
"Create a new uncloseai client with auto-discovery from environment variables"
|
||||
(let ((client (make-instance 'uncloseai
|
||||
:api-key api-key
|
||||
:timeout timeout)))
|
||||
(initialize-client client model-endpoints tts-endpoints)))
|
||||
|
||||
;;; Helper functions
|
||||
|
||||
(defun get-model-info (client model-id)
|
||||
"Get model info by ID or return first available model"
|
||||
(when (null (models client))
|
||||
(error "No models available. Check endpoint configuration."))
|
||||
|
||||
(if (null model-id)
|
||||
(first (models client))
|
||||
(or (find model-id (models client) :key (lambda (m) (getf m :id)) :test #'string=)
|
||||
(error "Model '~A' not found in discovered models" model-id))))
|
||||
|
||||
(defun make-headers (client &optional (content-type t))
|
||||
"Create HTTP headers with optional authorization"
|
||||
(let ((headers nil))
|
||||
(when content-type
|
||||
(push (cons "Content-Type" "application/json") headers))
|
||||
(when (api-key client)
|
||||
(push (cons "Authorization"
|
||||
(format nil "Bearer ~A" (api-key client)))
|
||||
headers))
|
||||
headers))
|
||||
|
||||
;;; Non-streaming chat completion
|
||||
|
||||
(defun chat (client messages &key model (max-tokens 100) (temperature 0.7))
|
||||
"Non-streaming chat completion
|
||||
|
||||
Args:
|
||||
messages - List of message plists with :role and :content
|
||||
model - Model ID (defaults to first available model)
|
||||
max-tokens - Maximum tokens in response
|
||||
temperature - Sampling temperature
|
||||
|
||||
Returns:
|
||||
Response plist with :choices containing the completion"
|
||||
(let* ((model-info (get-model-info client model))
|
||||
(endpoint (getf model-info :endpoint))
|
||||
(model-id (getf model-info :id))
|
||||
(url (concatenate 'string endpoint "/chat/completions"))
|
||||
(payload (jonathan:to-json
|
||||
(list :|model| model-id
|
||||
:|messages| (coerce messages 'vector)
|
||||
:|max_tokens| max-tokens
|
||||
:|temperature| temperature
|
||||
:|stream| :false)))
|
||||
(headers (make-headers client))
|
||||
(response (dex:post url
|
||||
:headers headers
|
||||
:content payload)))
|
||||
(jonathan:parse response)))
|
||||
|
||||
;;; Streaming chat completion
|
||||
|
||||
(defun process-sse-line (line)
|
||||
"Process a single Server-Sent Event line, return parsed content or nil"
|
||||
(when (and line (> (length line) 6) (string= (subseq line 0 6) "data: "))
|
||||
(let ((data (subseq line 6)))
|
||||
(unless (string= data "[DONE]")
|
||||
(handler-case
|
||||
(let* ((parsed (jonathan:parse data))
|
||||
(choices (getf parsed :|choices|))
|
||||
(delta (when (> (length choices) 0)
|
||||
(getf (aref choices 0) :|delta|)))
|
||||
(content (when delta (getf delta :|content|))))
|
||||
content)
|
||||
(error (e) nil))))))
|
||||
|
||||
(defun chat-stream (client messages callback &key model (max-tokens 500) (temperature 0.7))
|
||||
"Streaming chat completion using Server-Sent Events
|
||||
|
||||
Args:
|
||||
messages - List of message plists with :role and :content
|
||||
callback - Function to call for each content chunk (receives string)
|
||||
model - Model ID (defaults to first available model)
|
||||
max-tokens - Maximum tokens in response
|
||||
temperature - Sampling temperature"
|
||||
(let* ((model-info (get-model-info client model))
|
||||
(endpoint (getf model-info :endpoint))
|
||||
(model-id (getf model-info :id))
|
||||
(url (concatenate 'string endpoint "/chat/completions"))
|
||||
(payload (jonathan:to-json
|
||||
(list :|model| model-id
|
||||
:|messages| (coerce messages 'vector)
|
||||
:|max_tokens| max-tokens
|
||||
:|temperature| temperature
|
||||
:|stream| t)))
|
||||
(headers (make-headers client)))
|
||||
(dex:request url
|
||||
:method :post
|
||||
:headers headers
|
||||
:content payload
|
||||
:want-stream t
|
||||
:stream-callback
|
||||
(lambda (stream)
|
||||
(loop for line = (read-line stream nil nil)
|
||||
while line
|
||||
do (let ((content (process-sse-line line)))
|
||||
(when content
|
||||
(funcall callback content))))))))
|
||||
|
||||
;;; Text-to-Speech
|
||||
|
||||
(defun tts (client text &key (voice "alloy") (model "tts-1") (response-format "mp3"))
|
||||
"Generate speech from text
|
||||
|
||||
Args:
|
||||
text - Input text to convert to speech
|
||||
voice - Voice name (alloy, echo, fable, onyx, nova, shimmer)
|
||||
model - TTS model (tts-1 or tts-1-hd)
|
||||
response-format - Audio format (mp3, opus, aac, flac)
|
||||
|
||||
Returns:
|
||||
Audio data as byte array"
|
||||
(when (null (tts-endpoints client))
|
||||
(error "No TTS endpoints available"))
|
||||
|
||||
(let* ((endpoint (first (tts-endpoints client)))
|
||||
(url (concatenate 'string endpoint "/audio/speech"))
|
||||
(payload (jonathan:to-json
|
||||
(list :|model| model
|
||||
:|voice| voice
|
||||
:|input| text
|
||||
:|response_format| response-format)))
|
||||
(headers (make-headers client))
|
||||
(response (dex:post url
|
||||
:headers headers
|
||||
:content payload
|
||||
:force-binary t)))
|
||||
response))
|
||||
|
||||
;;; Demo usage
|
||||
|
||||
(defun demo-nonstreaming (client)
|
||||
"Demonstrate non-streaming chat"
|
||||
(format t "=== Non-Streaming Chat ===~%")
|
||||
(let* ((response (chat client
|
||||
(list (list :|role| "system"
|
||||
:|content| "You are a helpful AI assistant.")
|
||||
(list :|role| "user"
|
||||
:|content| "Explain quantum computing in one sentence."))
|
||||
:max-tokens 100
|
||||
:temperature 0.7))
|
||||
(model-used (getf response :|model|))
|
||||
(choice (aref (getf response :|choices|) 0))
|
||||
(message (getf choice :|message|))
|
||||
(content (getf message :|content|)))
|
||||
(format t "Model: ~A~%" model-used)
|
||||
(format t "Response: ~A~%~%" content)))
|
||||
|
||||
(defun demo-streaming (client)
|
||||
"Demonstrate streaming chat"
|
||||
(format t "=== Streaming Chat ===~%")
|
||||
(let ((model-id (if (> (length (models client)) 1)
|
||||
(getf (second (models client)) :id)
|
||||
nil)))
|
||||
(format t "Model: ~A~%" (or model-id (getf (first (models client)) :id)))
|
||||
(format t "Response: ")
|
||||
(force-output)
|
||||
|
||||
(chat-stream client
|
||||
(list (list :|role| "system"
|
||||
:|content| "You are a coding assistant.")
|
||||
(list :|role| "user"
|
||||
:|content| "Write a Common Lisp function to check if a number is prime"))
|
||||
(lambda (content)
|
||||
(format t "~A" content)
|
||||
(force-output))
|
||||
:model model-id
|
||||
:max-tokens 200
|
||||
:temperature 0.7)
|
||||
|
||||
(format t "~%~%")))
|
||||
|
||||
(defun demo-tts (client)
|
||||
"Demonstrate text-to-speech"
|
||||
(when (tts-endpoints client)
|
||||
(format t "=== TTS Speech Generation ===~%")
|
||||
(let ((audio-data (tts client
|
||||
"Hello from uncloseai Common Lisp client! This demonstrates text to speech with streaming support."
|
||||
:voice "alloy")))
|
||||
(with-open-file (out "speech.mp3"
|
||||
:direction :output
|
||||
:if-exists :supersede
|
||||
:if-does-not-exist :create
|
||||
:element-type '(unsigned-byte 8))
|
||||
(write-sequence audio-data out))
|
||||
(format t "[OK] Speech file created: speech.mp3 (~D bytes)~%~%" (length audio-data)))))
|
||||
|
||||
(defun main ()
|
||||
"Main demo function"
|
||||
(format t "=== uncloseai Common Lisp Client (with Streaming) ===~%~%")
|
||||
|
||||
;; Initialize client (auto-discovers from environment)
|
||||
(let ((client (make-uncloseai)))
|
||||
|
||||
(when (null (models client))
|
||||
(format t "ERROR: No models discovered. Set environment variables:~%")
|
||||
(format t " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.~%")
|
||||
(uiop:quit 1))
|
||||
|
||||
(format t "Discovered ~D model(s)~%" (length (models client)))
|
||||
(dolist (model (models client))
|
||||
(format t " - ~A (max_tokens: ~D)~%"
|
||||
(getf model :id)
|
||||
(getf model :max-tokens)))
|
||||
(format t "~%")
|
||||
|
||||
;; Run demos
|
||||
(handler-case
|
||||
(progn
|
||||
(demo-nonstreaming client)
|
||||
(demo-streaming client)
|
||||
(demo-tts client)
|
||||
(format t "=== Examples Complete ===~%"))
|
||||
(error (e)
|
||||
(format t "~%Error: ~A~%" e)))))
|
||||
|
||||
;; Main execution
|
||||
(main)
|
||||
|
|
@ -12,15 +12,11 @@ RUN apt-get update && \
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Scheme scripts
|
||||
COPY hermes-nonstreaming.scm .
|
||||
COPY hermes-streaming.scm .
|
||||
COPY qwen-nonstreaming.scm .
|
||||
COPY qwen-streaming.scm .
|
||||
COPY tts.scm .
|
||||
# Copy Scheme script
|
||||
COPY uncloseai.scm .
|
||||
|
||||
# Make scripts executable
|
||||
RUN chmod +x *.scm
|
||||
# Make script executable
|
||||
RUN chmod +x uncloseai.scm
|
||||
|
||||
# Default command shows available examples
|
||||
CMD ["sh", "-c", "echo 'Available examples:' && echo ' guile hermes-nonstreaming.scm' && echo ' guile hermes-streaming.scm' && echo ' guile qwen-nonstreaming.scm' && echo ' guile qwen-streaming.scm' && echo ' guile tts.scm'"]
|
||||
# Default command runs the demo
|
||||
CMD ["guile", "uncloseai.scm"]
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Hermes AI Non-Streaming Example in GNU Guile
|
||||
;;; Uses (web client) and (json) modules
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(json))
|
||||
|
||||
(define base-url "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(define model "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
;; Create JSON payload
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #f))))
|
||||
|
||||
(display "Requesting from Hermes AI...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
;; Make POST request
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
;; Parse JSON response
|
||||
(let* ((json-response (json-string->scm (utf8->string body)))
|
||||
(choices (assoc-ref json-response "choices"))
|
||||
(message (assoc-ref (vector-ref choices 0) "message"))
|
||||
(content (assoc-ref message "content")))
|
||||
(format #t "Response: ~a\n" content)))))
|
||||
(lambda (key . args)
|
||||
(format #t "Error: ~a ~a\n" key args)))
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Hermes AI Streaming Example in GNU Guile
|
||||
;;; Uses (web client) for streaming SSE responses
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(ice-9 rdelim)
|
||||
(json)
|
||||
(srfi srfi-1))
|
||||
|
||||
(define base-url "https://hermes.ai.unturf.com/v1/chat/completions")
|
||||
(define model "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||
|
||||
;; Create JSON payload for streaming
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #t))))
|
||||
|
||||
(display "Streaming from Hermes AI...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
;; Make streaming POST request
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))
|
||||
#:streaming? #t))
|
||||
(lambda (response port)
|
||||
;; Read and process SSE stream line by line
|
||||
(let loop ((line (read-line port)))
|
||||
(unless (eof-object? line)
|
||||
(when (string-prefix? "data: " line)
|
||||
(let ((json-data (substring line 6)))
|
||||
(unless (string=? json-data "[DONE]")
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((parsed (json-string->scm json-data))
|
||||
(choices (assoc-ref parsed "choices")))
|
||||
(when (and choices (> (vector-length choices) 0))
|
||||
(let* ((delta (assoc-ref (vector-ref choices 0) "delta"))
|
||||
(content (assoc-ref delta "content")))
|
||||
(when content
|
||||
(display content)
|
||||
(force-output))))))
|
||||
(lambda (key . args) #f)))))
|
||||
(loop (read-line port)))))
|
||||
(display "\n\nDone!\n"))))
|
||||
(lambda (key . args)
|
||||
(format #t "\nError: ~a ~a\n" key args)))
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Qwen 3 Coder Non-Streaming Example in GNU Guile
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(json))
|
||||
|
||||
(define base-url "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(define model "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #f))))
|
||||
|
||||
(display "Requesting from Qwen 3 Coder...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
(let* ((json-response (json-string->scm (utf8->string body)))
|
||||
(choices (assoc-ref json-response "choices"))
|
||||
(message (assoc-ref (vector-ref choices 0) "message"))
|
||||
(content (assoc-ref message "content")))
|
||||
(format #t "Response: ~a\n" content)))))
|
||||
(lambda (key . args)
|
||||
(format #t "Error: ~a ~a\n" key args)))
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Qwen 3 Coder Streaming Example in GNU Guile
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(ice-9 rdelim)
|
||||
(json)
|
||||
(srfi srfi-1))
|
||||
|
||||
(define base-url "https://qwen.ai.unturf.com/v1/chat/completions")
|
||||
(define model "hf.co/unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M")
|
||||
|
||||
(define payload
|
||||
(scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . #(((role . "user")
|
||||
(content . "Give a Python Fizzbuzz solution in one line of code?"))))
|
||||
(temperature . 0.5)
|
||||
(max_tokens . 150)
|
||||
(stream . #t))))
|
||||
|
||||
(display "Streaming from Qwen 3 Coder...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))
|
||||
#:streaming? #t))
|
||||
(lambda (response port)
|
||||
(let loop ((line (read-line port)))
|
||||
(unless (eof-object? line)
|
||||
(when (string-prefix? "data: " line)
|
||||
(let ((json-data (substring line 6)))
|
||||
(unless (string=? json-data "[DONE]")
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((parsed (json-string->scm json-data))
|
||||
(choices (assoc-ref parsed "choices")))
|
||||
(when (and choices (> (vector-length choices) 0))
|
||||
(let* ((delta (assoc-ref (vector-ref choices 0) "delta"))
|
||||
(content (assoc-ref delta "content")))
|
||||
(when content
|
||||
(display content)
|
||||
(force-output))))))
|
||||
(lambda (key . args) #f)))))
|
||||
(loop (read-line port)))))
|
||||
(display "\n\nDone!\n"))))
|
||||
(lambda (key . args)
|
||||
(format #t "\nError: ~a ~a\n" key args)))
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; Text-to-Speech Example in GNU Guile
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 binary-ports)
|
||||
(json))
|
||||
|
||||
(define base-url "https://speech.ai.unturf.com/v1/audio/speech")
|
||||
(define output-file "speech.mp3")
|
||||
|
||||
(define payload
|
||||
(scm->json-string
|
||||
'((model . "tts-1")
|
||||
(voice . "alloy")
|
||||
(speed . 0.9)
|
||||
(input . "I think so therefore, Today is a wonderful day to grow something people love!"))))
|
||||
|
||||
(display "Generating speech from TTS...\n\n")
|
||||
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post base-url
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
;; Write binary response to file
|
||||
(call-with-output-file output-file
|
||||
(lambda (port)
|
||||
(put-bytevector port body)))
|
||||
(format #t "Speech saved to: ~a\n" output-file))))
|
||||
(lambda (key . args)
|
||||
(format #t "Error: ~a ~a\n" key args)))
|
||||
263
public/languages/scheme/uncloseai.scm
Normal file
263
public/languages/scheme/uncloseai.scm
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/env guile
|
||||
!#
|
||||
|
||||
;;; UncloseAI GNU Guile Library - OpenAI-compatible API client with streaming support
|
||||
;;; Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
|
||||
;;;
|
||||
;;; Library Functions:
|
||||
;;; (uncloseai-init) - Initialize client with model discovery
|
||||
;;; (uncloseai-list-models) - List discovered models
|
||||
;;; (uncloseai-chat messages model-id) - Non-streaming chat completion
|
||||
;;; (uncloseai-chat-stream messages model-id) - Streaming chat completion
|
||||
;;; (uncloseai-tts text voice model) - Text-to-speech generation
|
||||
|
||||
(use-modules (web client)
|
||||
(web response)
|
||||
(ice-9 textual-ports)
|
||||
(ice-9 rdelim)
|
||||
(ice-9 binary-ports)
|
||||
(json)
|
||||
(srfi srfi-1))
|
||||
|
||||
;;; Global client state
|
||||
(define *model-ids* '())
|
||||
(define *model-endpoints* '())
|
||||
(define *model-max-tokens* '())
|
||||
(define *tts-endpoints* '())
|
||||
|
||||
;;; Initialize client and discover models from environment variables
|
||||
(define (uncloseai-init)
|
||||
(set! *model-ids* '())
|
||||
(set! *model-endpoints* '())
|
||||
(set! *model-max-tokens* '())
|
||||
(set! *tts-endpoints* '())
|
||||
|
||||
;; Discover chat/code models from MODEL_ENDPOINT_N (N = 1 to 9999)
|
||||
(let loop ((i 1))
|
||||
(when (<= i 9999)
|
||||
(let ((endpoint (getenv (string-append "MODEL_ENDPOINT_" (number->string i)))))
|
||||
(if endpoint
|
||||
(begin
|
||||
;; Fetch models from endpoint
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-get (string-append endpoint "/models")))
|
||||
(lambda (response body)
|
||||
(when (= (response-code response) 200)
|
||||
(let* ((json-response (json-string->scm (utf8->string body)))
|
||||
(models-data (assoc-ref json-response "data")))
|
||||
(when models-data
|
||||
;; Process each model in the response
|
||||
(vector-for-each
|
||||
(lambda (model)
|
||||
(let ((model-id (assoc-ref model "id")))
|
||||
;; Skip modelperm entries
|
||||
(when (and model-id
|
||||
(not (string-prefix? "modelperm-" model-id)))
|
||||
;; Extract max_model_len if available (vLLM)
|
||||
(let ((max-tokens (or (assoc-ref model "max_model_len") 8192)))
|
||||
(set! *model-ids* (append *model-ids* (list model-id)))
|
||||
(set! *model-endpoints* (append *model-endpoints* (list endpoint)))
|
||||
(set! *model-max-tokens* (append *model-max-tokens* (list max-tokens)))))))
|
||||
models-data)))))))
|
||||
(lambda (key . args)
|
||||
;; Silently skip endpoints that fail
|
||||
#f))
|
||||
(loop (+ i 1)))
|
||||
#f))))
|
||||
|
||||
;; Discover TTS endpoints from TTS_ENDPOINT_N (N = 1 to 9999)
|
||||
(let loop ((i 1))
|
||||
(when (<= i 9999)
|
||||
(let ((endpoint (getenv (string-append "TTS_ENDPOINT_" (number->string i)))))
|
||||
(if endpoint
|
||||
(begin
|
||||
(set! *tts-endpoints* (append *tts-endpoints* (list endpoint)))
|
||||
(loop (+ i 1)))
|
||||
#f))))
|
||||
|
||||
(length *model-ids*))
|
||||
|
||||
;;; List all discovered models
|
||||
(define (uncloseai-list-models)
|
||||
(for-each
|
||||
(lambda (model-id max-tokens)
|
||||
(format #t " - ~a (max_tokens: ~a)\n" model-id max-tokens))
|
||||
*model-ids*
|
||||
*model-max-tokens*))
|
||||
|
||||
;;; Get model index by ID or return 0 for first model
|
||||
(define (uncloseai-get-model-idx model-id)
|
||||
(if (or (not model-id) (string=? model-id ""))
|
||||
0 ;; Return first model index
|
||||
(let loop ((i 0))
|
||||
(cond
|
||||
((>= i (length *model-ids*)) -1) ;; Not found
|
||||
((string=? (list-ref *model-ids* i) model-id) i)
|
||||
(else (loop (+ i 1)))))))
|
||||
|
||||
;;; Non-streaming chat completion
|
||||
;;; Usage: (uncloseai-chat messages model-id max-tokens temperature)
|
||||
;;; messages: vector of message objects like #(((role . "user") (content . "...")))
|
||||
;;; Returns: content string
|
||||
(define* (uncloseai-chat messages #:optional (model-id "") (max-tokens 100) (temperature 0.7))
|
||||
(let ((model-idx (uncloseai-get-model-idx model-id)))
|
||||
(if (= model-idx -1)
|
||||
"ERROR: Model not found"
|
||||
(let ((endpoint (list-ref *model-endpoints* model-idx))
|
||||
(model (list-ref *model-ids* model-idx)))
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let ((payload (scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . ,messages)
|
||||
(max_tokens . ,max-tokens)
|
||||
(temperature . ,temperature)
|
||||
(stream . #f)))))
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post (string-append endpoint "/chat/completions")
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
(if (= (response-code response) 200)
|
||||
(let* ((json-response (json-string->scm (utf8->string body)))
|
||||
(choices (assoc-ref json-response "choices"))
|
||||
(message (assoc-ref (vector-ref choices 0) "message"))
|
||||
(content (assoc-ref message "content")))
|
||||
content)
|
||||
"ERROR: Request failed")))))
|
||||
(lambda (key . args)
|
||||
(format #f "ERROR: ~a" key)))))))
|
||||
|
||||
;;; Streaming chat completion with SSE parsing
|
||||
;;; Usage: (uncloseai-chat-stream messages model-id max-tokens temperature)
|
||||
;;; messages: vector of message objects
|
||||
;;; Prints content chunks as they arrive
|
||||
(define* (uncloseai-chat-stream messages #:optional (model-id "") (max-tokens 500) (temperature 0.7))
|
||||
(let ((model-idx (uncloseai-get-model-idx model-id)))
|
||||
(if (= model-idx -1)
|
||||
(display "ERROR: Model not found\n")
|
||||
(let ((endpoint (list-ref *model-endpoints* model-idx))
|
||||
(model (list-ref *model-ids* model-idx)))
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let ((payload (scm->json-string
|
||||
`((model . ,model)
|
||||
(messages . ,messages)
|
||||
(max_tokens . ,max-tokens)
|
||||
(temperature . ,temperature)
|
||||
(stream . #t)))))
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post (string-append endpoint "/chat/completions")
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))
|
||||
#:streaming? #t))
|
||||
(lambda (response port)
|
||||
;; Read and process SSE stream line by line
|
||||
(let loop ((line (read-line port)))
|
||||
(unless (eof-object? line)
|
||||
(when (string-prefix? "data: " line)
|
||||
(let ((json-data (substring line 6)))
|
||||
(unless (string=? json-data "[DONE]")
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let* ((parsed (json-string->scm json-data))
|
||||
(choices (assoc-ref parsed "choices")))
|
||||
(when (and choices (> (vector-length choices) 0))
|
||||
(let* ((delta (assoc-ref (vector-ref choices 0) "delta"))
|
||||
(content (assoc-ref delta "content")))
|
||||
(when content
|
||||
(display content)
|
||||
(force-output))))))
|
||||
(lambda (key . args) #f)))))
|
||||
(loop (read-line port))))))))
|
||||
(lambda (key . args)
|
||||
(format #t "\nERROR: ~a\n" key)))))))
|
||||
|
||||
;;; Text-to-speech generation
|
||||
;;; Usage: (uncloseai-tts text voice model output-file)
|
||||
;;; Returns: file size in bytes (0 on error)
|
||||
(define* (uncloseai-tts text #:optional (voice "alloy") (model "tts-1") (output-file "/tmp/speech.mp3"))
|
||||
(if (null? *tts-endpoints*)
|
||||
(begin
|
||||
(display "ERROR: No TTS endpoints available\n")
|
||||
0)
|
||||
(let ((endpoint (car *tts-endpoints*)))
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(let ((payload (scm->json-string
|
||||
`((model . ,model)
|
||||
(voice . ,voice)
|
||||
(input . ,text)))))
|
||||
(call-with-values
|
||||
(lambda ()
|
||||
(http-post (string-append endpoint "/audio/speech")
|
||||
#:body payload
|
||||
#:headers '((Content-Type . "application/json"))))
|
||||
(lambda (response body)
|
||||
(if (= (response-code response) 200)
|
||||
(begin
|
||||
;; Write binary response to file
|
||||
(call-with-output-file output-file
|
||||
(lambda (port)
|
||||
(put-bytevector port body)))
|
||||
(stat:size (stat output-file)))
|
||||
0)))))
|
||||
(lambda (key . args)
|
||||
0)))))
|
||||
|
||||
;;; Demo usage when run as script
|
||||
(when (batch-mode?)
|
||||
(display "=== UncloseAI GNU Guile Client (with Streaming) ===\n\n")
|
||||
|
||||
;; Initialize client with model discovery
|
||||
(let ((model-count (uncloseai-init)))
|
||||
(if (= model-count 0)
|
||||
(begin
|
||||
(display "ERROR: No models discovered. Set environment variables:\n")
|
||||
(display " MODEL_ENDPOINT_1, MODEL_ENDPOINT_2, etc.\n")
|
||||
(exit 1))
|
||||
(begin
|
||||
(format #t "Discovered ~a model(s)\n" model-count)
|
||||
(uncloseai-list-models)
|
||||
(display "\n")
|
||||
|
||||
;; Non-streaming chat example
|
||||
(display "=== Non-Streaming Chat ===\n")
|
||||
(let* ((messages #(((role . "system")
|
||||
(content . "You are a helpful AI assistant."))
|
||||
((role . "user")
|
||||
(content . "Explain quantum computing in one sentence."))))
|
||||
(response (uncloseai-chat messages)))
|
||||
(format #t "Model: ~a\n" (car *model-ids*))
|
||||
(format #t "Response: ~a\n\n" response))
|
||||
|
||||
;; Streaming chat example
|
||||
(display "=== Streaming Chat ===\n")
|
||||
(let* ((model-id (if (>= model-count 2)
|
||||
(list-ref *model-ids* 1)
|
||||
(car *model-ids*)))
|
||||
(messages #(((role . "system")
|
||||
(content . "You are a coding assistant."))
|
||||
((role . "user")
|
||||
(content . "Write a Scheme function to calculate factorial.")))))
|
||||
(format #t "Model: ~a\n" model-id)
|
||||
(display "Response: ")
|
||||
(uncloseai-chat-stream messages model-id 200)
|
||||
(display "\n\n"))
|
||||
|
||||
;; TTS example
|
||||
(when (not (null? *tts-endpoints*))
|
||||
(display "=== TTS Speech Generation ===\n")
|
||||
(let* ((text "Hello from UncloseAI GNU Guile client! This demonstrates text to speech with streaming support.")
|
||||
(output-file "/tmp/speech.mp3")
|
||||
(file-size (uncloseai-tts text "alloy" "tts-1" output-file)))
|
||||
(if (> file-size 0)
|
||||
(format #t "[OK] Speech file created: ~a (~a bytes)\n\n" output-file file-size)
|
||||
(display "[ERROR] TTS generation failed\n\n"))))
|
||||
|
||||
(display "=== Examples Complete ===\n")))))
|
||||
Loading…
Add table
Add a link
Reference in a new issue