From 358bbaaf2cc65b2d775d75734c0fb15cc9f2c109 Mon Sep 17 00:00:00 2001 From: Paul Nelson <63298781+ultronozm@users.noreply.github.com> Date: Wed, 22 Jan 2025 06:34:25 +0100 Subject: [PATCH] Add pdf input for Sonnet (#142) * llm-claude.el (llm-claude--multipart-content): Handle PDF's in multipart content. (llm-capabilities): Add pdf-input. * llm-integration-test.el (llm-pdf-chat): New integration test. * llm-models.el (llm-model): Add pdf-input to capabilities docstring. (llm-models): Add pdf-input to capabilities of Claude 3.5 Sonnet. * llm.el (llm-media): (llm-capabilities): Add pdf-input to documentation. --- llm-claude.el | 30 +++++++++++++++++------------- llm-integration-test.el | 17 +++++++++++++++++ llm-models.el | 6 +++--- llm.el | 6 ++++-- test.pdf | Bin 0 -> 17336 bytes 5 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 test.pdf diff --git a/llm-claude.el b/llm-claude.el index d3d308a..87646e3 100644 --- a/llm-claude.el +++ b/llm-claude.el @@ -92,18 +92,22 @@ (defun llm-claude--multipart-content (content) "Return CONTENT as a list of Claude multipart content." - (vconcat (mapcar (lambda (part) - (cond ((stringp part) - `(:type "text" - :text ,part)) - ((llm-media-p part) - `(:type "image" - :source (:type "base64" - :media_type ,(llm-media-mime-type part) - :data ,(base64-encode-string (llm-media-data part) t)))) - (t - (error "Unsupported multipart content: %s" part)))) - (llm-multipart-parts content)))) + (vconcat + (mapcar (lambda (part) + (cond ((stringp part) + `(:type "text" + :text ,part)) + ((llm-media-p part) + (let ((source (list :type "base64" + :media_type (llm-media-mime-type part) + :data (base64-encode-string (llm-media-data part) t)))) + `(:type ,(if (equal (llm-media-mime-type part) "application/pdf") + "document" + "image") + :source ,source))) + (t + (error "Unsupported multipart content: %s" part)))) + (llm-multipart-parts content)))) (cl-defmethod llm-provider-extract-tool-uses ((_ llm-claude) response) (let ((content (append (assoc-default 'content response) nil))) @@ -178,7 +182,7 @@ "Claude") (cl-defmethod llm-capabilities ((_ llm-claude)) - (list 'streaming 'function-calls 'image-input)) + (list 'streaming 'function-calls 'image-input 'pdf-input)) (cl-defmethod llm-provider-append-to-prompt ((_ llm-claude) prompt result &optional tool-use-results) diff --git a/llm-integration-test.el b/llm-integration-test.el index 5303c44..7a0c3e2 100644 --- a/llm-integration-test.el +++ b/llm-integration-test.el @@ -290,6 +290,23 @@ else. We really just want to see if it's in the right ballpark." (should (stringp result)) (should (llm-integration-test-string-eq "owl" (string-trim (downcase result))))))) +(llm-def-integration-test + llm-pdf-chat (provider) + (when (member 'pdf-input (llm-capabilities provider)) + (let* ((pdf-data + (with-temp-buffer (set-buffer-multibyte nil) + (insert-file-contents-literally + (expand-file-name "test.pdf" llm-integration-current-directory)) + (buffer-string))) + (result (llm-chat + provider + (llm-make-chat-prompt + (llm-make-multipart + "What symbol occurs in the PDF file? If you do not see a PDF file, please let me know. If you do, please answer in one letter, without punctuation or whitespace." + (make-llm-media :mime-type "application/pdf" :data pdf-data)))))) + (should (stringp result)) + (should (llm-integration-test-string-eq "x" (string-trim (downcase result))))))) + (llm-def-integration-test llm-json-test (provider) (when (member 'json-response (llm-capabilities provider)) (let ((result (llm-chat diff --git a/llm-models.el b/llm-models.el index df4eee7..dee1f5e 100644 --- a/llm-models.el +++ b/llm-models.el @@ -30,8 +30,8 @@ NAME is the name of the model, appropriate for showing a user. CAPABILITIES is a list of symbols representing the capabilities of the model, one of `embedding', `generation', `tool-use', -`image-input', `image-output', `audio-input', `video-input', `caching' -and `free-software'. +`image-input', `image-output', `audio-input', `video-input', 'pdf-input', +`caching' and `free-software'. REGEX is a regular expression that can be used to identify the model, uniquely (it shouldn't conflict with any other model)" name @@ -103,7 +103,7 @@ REGEX is a regular expression that can be used to identify the model, uniquely ( ;; https://docs.anthropic.com/en/docs/about-claude/models (make-llm-model :name "Claude 3.5 Sonnet" :symbol 'claude-3.5-sonnet - :capabilities '(generation tool-use image-input caching) + :capabilities '(generation tool-use image-input pdf-input caching) :context-length 200000 :regex "claude-3.5-sonnet") (make-llm-model diff --git a/llm.el b/llm.el index 9056355..42bea1c 100644 --- a/llm.el +++ b/llm.el @@ -134,8 +134,8 @@ MIME types are accepted by all providers. DATA is a (binary) string containing the data. The string should use unibyte encoding. -This should only be used if the `image-input' or `audio-input' -capability is available, as indicated by `llm-capabilities'." +This should only be used if the `image-input' or `audio-input' or +`pdf-input' capability is available, as indicated by `llm-capabilities'." mime-type data) (defun llm--image-to-media (image) @@ -556,6 +556,8 @@ won't have any partial responses, so basically just operates like `image-input': the LLM can accept images as input. +`pdf-input': the LLM can accept PDF documents as input. + `json-response': the LLM can be requested to return responses only in JSON format. diff --git a/test.pdf b/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2b1900d43b43264b1e0d3ea3cb9699b4c7720316 GIT binary patch literal 17336 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 B?oj{$ literal 0 HcmV?d00001