event-stream.scm 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. ;;; guile-openai --- An OpenAI API client for Guile
  2. ;;; Copyright © 2023 Andrew Whatson <whatson@tailcall.au>
  3. ;;;
  4. ;;; This file is part of guile-openai.
  5. ;;;
  6. ;;; guile-openai is free software: you can redistribute it and/or modify
  7. ;;; it under the terms of the GNU Affero General Public License as
  8. ;;; published by the Free Software Foundation, either version 3 of the
  9. ;;; License, or (at your option) any later version.
  10. ;;;
  11. ;;; guile-openai is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. ;;; Affero General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU Affero General Public
  17. ;;; License along with guile-openai. If not, see
  18. ;;; <https://www.gnu.org/licenses/>.
  19. (define-module (openai utils event-stream)
  20. #:use-module (ice-9 rdelim)
  21. #:use-module (srfi srfi-41)
  22. #:export (port->line-stream
  23. line-stream->event-stream))
  24. (define (read-crlf-line port)
  25. "Read a single line from PORT, delimited by either CR, LF, or CRLF.
  26. The delimiter is included in the returned string. Returns #f at
  27. end-of-file."
  28. (let ((line (read-delimited "\r\n" port 'peek)))
  29. (and (string? line)
  30. (let* ((del1 (read-char port))
  31. (del1 (and (char? del1) del1))
  32. (del2 (and (eqv? del1 #\return)
  33. (eqv? (peek-char port) #\newline)
  34. (read-char port))))
  35. (cond ((and del1 del2)
  36. (string-append line (string del1 del2)))
  37. (del1
  38. (string-append line (string del1)))
  39. (else line))))))
  40. (define (chomp-prefix prefix line)
  41. "Return the substring between PREFIX at the start of LINE, and a
  42. CR/LF/CRLF delimiter at the end of LINE. Returns #f if LINE doesn't
  43. start with PREFIX."
  44. (and (string-prefix? prefix line)
  45. (let* ((len (string-length line))
  46. (del1 (string-ref line (- len 2)))
  47. (del2 (string-ref line (- len 1)))
  48. (offset (cond ((eqv? del1 #\return) 2)
  49. ((eqv? del2 #\return) 1)
  50. ((eqv? del2 #\newline) 1)
  51. (else 0))))
  52. (substring/shared line
  53. (string-length prefix)
  54. (- len offset)))))
  55. (define (port->line-stream port)
  56. "Return a stream which will yield each CR/LF/CRLF delimited line read
  57. from PORT."
  58. (stream-let loop ()
  59. (let ((line (read-crlf-line port)))
  60. (if line
  61. (stream-cons line (loop))
  62. stream-null))))
  63. (define (line-stream->event-stream strm)
  64. "Return a stream yielding the text/event-stream data payloads from the
  65. line-stream STRM."
  66. (stream-let loop ((strm strm))
  67. (if (stream-null? strm)
  68. stream-null
  69. (let* ((line (stream-car strm))
  70. (data (chomp-prefix "data: " line))
  71. (rest (stream-cdr strm)))
  72. (cond ((not data)
  73. (loop rest))
  74. ((string=? data "[DONE]")
  75. stream-null)
  76. (else
  77. (stream-cons data (loop rest))))))))