branch: externals/llm
commit 63f2b8ffbc1f593418e93a9e21ef6ed5c1e70c3a
Merge: c9ab8664ce 6c6ef8270e
Author: Andrew Hyatt <ahy...@gmail.com>
Commit: Andrew Hyatt <ahy...@gmail.com>

    Merge branch 'main' into plz
---
 README.org    |   5 ++
 llm-claude.el | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 llm-tester.el |  26 +++++++--
 3 files changed, 201 insertions(+), 5 deletions(-)

diff --git a/README.org b/README.org
index 44306b784d..2646c043e9 100644
--- a/README.org
+++ b/README.org
@@ -49,6 +49,11 @@ In addition to the provider, which you may want multiple of 
(for example, to cha
   #+begin_src sh
   gcloud beta services identity create --service=aiplatform.googleapis.com 
--project=PROJECT_ID
   #+end_src
+** Claude
+[[https://docs.anthropic.com/claude/docs/intro-to-claude][Claude]] is 
Anthropic's large language model.  It does not support embeddings.  You can set 
it up with the following parameters:
+
+=:key=: The API key you get from 
[[https://console.anthropic.com/settings/keys][Claude's settings page]].  This 
is required.
+=:chat-model=: One of the 
[[https://docs.anthropic.com/claude/docs/models-overview][Claude models]].  
Defaults to "claude-3-opus-20240229", the most powerful model.
 ** Ollama
 [[https://ollama.ai/][Ollama]] is a way to run large language models locally. 
There are [[https://ollama.ai/library][many different models]] you can use with 
it. You set it up with the following parameters:
 - ~:scheme~: The scheme (http/https) for the connection to ollama.  This 
default to "http".
diff --git a/llm-claude.el b/llm-claude.el
new file mode 100644
index 0000000000..21af406c2d
--- /dev/null
+++ b/llm-claude.el
@@ -0,0 +1,175 @@
+;;; llm-claude.el --- llm module for integrating with Claude -*- 
lexical-binding: t; package-lint-main-file: "llm.el"; -*-
+
+;; Copyright (c) 2024  Free Software Foundation, Inc.
+
+;; Author: Andrew Hyatt <ahy...@gmail.com>
+;; Homepage: https://github.com/ahyatt/llm
+;; SPDX-License-Identifier: GPL-3.0-or-later
+;;
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation; either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;; This file implements the llm functionality defined in llm.el, for Claude's
+;; API.
+
+;;; Code:
+
+(require 'llm)
+(require 'llm-request)
+(require 'llm-provider-utils)
+(require 'rx)
+
+;; Models defined at https://docs.anthropic.com/claude/docs/models-overview
+(cl-defstruct llm-claude
+  (key nil :read-only t)
+  (chat-model "claude-3-opus-20240229" :read-only t))
+
+(defun llm-claude-check-key (provider)
+  "Check if the API key is valid, error if not."
+  (unless (llm-claude-key provider)
+    (error "No API key provided for Claude")))
+
+(defun llm-claude-request (provider prompt stream)
+  "Return the request (as an elisp JSON-convertable object).
+PROVIDER contains the model name.
+PROMPT is a `llm-chat-prompt' struct.
+STREAM is a boolean indicating whether the response should be streamed."
+  (let ((request `(("model" . ,(llm-claude-chat-model provider))
+                   ("stream" . ,(if stream t :json-false))
+                   ;; Claude requires max_tokens
+                   ("max_tokens" . ,(or (llm-chat-prompt-max-tokens prompt) 
4096))
+                   ("messages" .
+                    ,(mapcar (lambda (interaction)
+                               `(("role" . ,(llm-chat-prompt-interaction-role 
interaction))
+                                 ("content" . 
,(llm-chat-prompt-interaction-content interaction))))
+                             (llm-chat-prompt-interactions prompt)))))
+        (system (llm-provider-utils-get-system-prompt prompt)))
+    (when (> (length system) 0)
+      (push `("system" . ,system) request))
+    (when (llm-chat-prompt-temperature prompt)
+      (push `("temperature" . ,(llm-chat-prompt-temperature prompt)) request))
+    request))
+
+(defun llm-claude-get-response (response)
+  "Return the content of the response from the returned value."
+  (let ((content (aref (assoc-default 'content response) 0)))
+    (if (equal (assoc-default 'type content) "text")
+        (assoc-default 'text content)
+      (format "Unsupported non-text response: %s" content))))
+
+;; see https://docs.anthropic.com/claude/reference/messages-streaming
+(defun llm-claude-get-partial-response (response)
+  "Return the partial response from text RESPONSE."
+  (let ((regex (rx (seq "\"text\":" (0+ whitespace)
+                        (group-n 1 ?\" (* anychar) ?\") "}}"))))
+    (with-temp-buffer
+      (insert response)
+      ;; We use the quick and dirty solution of just looking for any line that
+      ;; has a "text" field.
+      (let ((matched-lines))
+        (goto-char (point-min))
+        (while (re-search-forward "\"text\":" nil t)
+          (push (buffer-substring-no-properties
+                 (line-beginning-position)
+                 (line-end-position))
+                matched-lines))
+        (mapconcat (lambda (line)
+                     (string-match regex line)
+                     (read (match-string 1 line)))
+                   (nreverse matched-lines) "")))))
+
+(cl-defmethod llm-chat ((provider llm-claude) prompt)
+  (llm-claude-check-key provider)
+  (let ((content (llm-claude-get-response
+                  (llm-request-sync "https://api.anthropic.com/v1/messages";
+                                    :headers `(("x-api-key" . ,(llm-claude-key 
provider))
+                                               ("anthropic-version" . 
"2023-06-01"))
+                                    :data (llm-claude-request provider prompt 
nil)))))
+    (llm-provider-utils-append-to-prompt prompt content)
+    content))
+
+(cl-defmethod llm-chat-async ((provider llm-claude) prompt response-callback 
error-callback)
+  (llm-claude-check-key provider)
+  (let ((buf (current-buffer)))
+    (llm-request-async "https://api.anthropic.com/v1/messages";
+                       :headers `(("x-api-key" . ,(llm-claude-key provider))
+                                  ("anthropic-version" . "2023-06-01"))
+                       :data (llm-claude-request provider prompt nil)
+                       :on-success
+                       (lambda (response)
+                         (let ((content (llm-claude-get-response response)))
+                           (llm-provider-utils-append-to-prompt prompt content)
+                           (llm-request-callback-in-buffer
+                            buf
+                            response-callback
+                            content)))
+                       :on-error
+                       (lambda (_ msg)
+                         (message "Error: %s" msg)
+                         (let ((error (assoc-default 'error msg)))
+                           (llm-request-callback-in-buffer
+                            buf error-callback
+                            'error
+                            (format "%s: %s" (assoc-default 'type error)
+                                    (assoc-default 'message error))))))))
+
+(cl-defmethod llm-chat-streaming ((provider llm-claude) prompt partial-callback
+                                  response-callback error-callback)
+  (llm-claude-check-key provider)
+  (let ((buf (current-buffer)))
+    (llm-request-async "https://api.anthropic.com/v1/messages";
+                       :headers `(("x-api-key" . ,(llm-claude-key provider))
+                                  ("anthropic-version" . "2023-06-01"))
+                       :data (llm-claude-request provider prompt t)
+                       :on-partial
+                       (lambda (data)
+                         (llm-request-callback-in-buffer
+                          buf
+                          partial-callback
+                          (llm-claude-get-partial-response data)))
+                       :on-success-raw
+                       (lambda (response)
+                         (let ((content
+                                (llm-claude-get-partial-response response)))
+                           (llm-provider-utils-append-to-prompt prompt content)
+                           (llm-request-callback-in-buffer
+                            buf
+                            response-callback
+                            content)))
+                       :on-error
+                       (lambda (_ msg)
+                         (message "Error: %s" msg)
+                         (let ((error (assoc-default 'error msg)))
+                           (llm-request-callback-in-buffer
+                            buf error-callback
+                            'error
+                            (format "%s: %s" (assoc-default 'type error)
+                                    (assoc-default 'message error))))))))
+
+;; See https://docs.anthropic.com/claude/docs/models-overview
+(cl-defmethod llm-chat-token-limit ((provider llm-claude))
+  (pcase (llm-claude-chat-model provider)
+    ("claude-2.0" 100000)
+    ("claude-instant-1.2" 100000)
+    (_ 200000)))
+
+(cl-defmethod llm-name ((_ llm-claude))
+  "Claude")
+
+(cl-defmethod llm-capabilities ((_ llm-claude))
+  (list 'streaming))
+
+(provide 'llm-claude)
+
+;;; llm-claude.el ends here
diff --git a/llm-tester.el b/llm-tester.el
index 7a284d9a27..f5fd9fdfa0 100644
--- a/llm-tester.el
+++ b/llm-tester.el
@@ -333,28 +333,44 @@ of by calling the `describe_function' function."
     (llm-tester-log "SUCCESS: Provider %s cancelled an async request" (type-of 
provider))
     (llm-cancel-request chat-async-request)))
 
-(defun llm-tester-all (provider)
-  "Test all llm functionality for PROVIDER."
-  (let ((separator (string-pad "" 30 ?=)))
+(defun llm-tester-all (provider &optional delay)
+  "Test all llm functionality for PROVIDER.
+DELAY is the number of seconds to wait between tests.  The
+default is 1.  Delays can help avoid rate limiting."
+  (let ((separator (string-pad "" 30 ?=))
+        (delay (or delay 1)))
     (llm-tester-log "\n%s\nTesting for %s\n%s\n"
                     separator (type-of provider) separator)
     (when (member 'embedding (llm-capabilities provider))
       (llm-tester-embedding-sync provider)
-      (llm-tester-embedding-async provider))
+      (sleep-for delay)
+      (llm-tester-embedding-async provider)
+      (sleep-for delay))
     (llm-tester-chat-sync provider)
+    (sleep-for delay)
     (llm-tester-chat-async provider)
+    (sleep-for delay)
     (llm-tester-chat-streaming provider)
+    (sleep-for delay)
     (llm-tester-chat-conversation-sync provider)
+    (sleep-for delay)
     (llm-tester-chat-conversation-async provider)
+    (sleep-for delay)
     (llm-tester-chat-conversation-streaming provider)
+    (sleep-for delay)
     ;; This is too flaky at the moment, subject to race conditions.
     ;; (llm-tester-cancel provider)
     (when (member 'function-calls (llm-capabilities provider))
       (llm-tester-function-calling-sync provider)
+      (sleep-for delay)
       (llm-tester-function-calling-async provider)
+      (sleep-for delay)
       (llm-tester-function-calling-streaming provider)
+      (sleep-for delay)
       (llm-tester-function-calling-conversation-sync provider)
-      (llm-tester-function-calling-conversation-async provider))
+      (sleep-for delay)
+      (llm-tester-function-calling-conversation-async provider)
+      (sleep-for delay))
     (sleep-for 10)
     (llm-tester-log "%s\nEnd of testing for %s\n\n"
                     separator (type-of provider))))

Reply via email to