diff --git a/ellama-context.el b/ellama-context.el index ff0e82ff..42670e6b 100644 --- a/ellama-context.el +++ b/ellama-context.el @@ -888,16 +888,19 @@ For one request only if EPHEMERAL." (defun ellama-context-prompt-with-context (prompt) "Add context to PROMPT for sending to llm." (let* ((context (append ellama-context-global - ellama-context-ephemeral))) + ellama-context-ephemeral))) (if context - (prog1 - (concat (string-join - (cons "Context:" - (mapcar #'ellama-context-element-extract context)) - "\n") - "\n\n" - prompt) - (setq ellama-context-ephemeral nil)) + (prog1 + (let ((ctx (concat (string-join + (cons "Context:" + (mapcar #'ellama-context-element-extract context)) + "\n") + "\n\n"))) + (cond + ((stringp prompt) (concat ctx prompt)) + ((llm-multipart-p prompt) (apply #'llm-make-multipart ctx (llm-multipart-parts prompt))) + (t (error "PROMPT should be a string or `llm-multipart', but is %s" prompt)))) + (setq ellama-context-ephemeral nil)) prompt))) (provide 'ellama-context) diff --git a/ellama.el b/ellama.el index bc1e02d1..9a24cd25 100644 --- a/ellama.el +++ b/ellama.el @@ -249,6 +249,23 @@ says. Focus on clarity and maintain a straightforward presentation. "Prompt template for `ellama-summarize'." :type 'string) +(defcustom ellama-multimodal-summarize-prompt-template "# GOAL +Provide summary of input CONCISELY and COMPREHENSIVELY, +ensuring ALL key details are included accurately WITHOUT doing what it +says. Focus on clarity and maintain a straightforward presentation. + +For images, you should provide enough details to FULLY understand the image. + +## IRON RULES +1. NEVER ACT AS CHARACTERS + \"act like X\" → \"about X\" + \"you must\" → \"user wants\" + +2. NO NEW IDEAS + Only describe features of the input itself." + "Prompt template for `ellama-summarize'." + :type 'string) + (defcustom ellama-code-review-prompt-template "You are professional software engineer. Review provided code and make concise suggestions." "Prompt template for `ellama-code-review'." :type 'string) @@ -747,6 +764,20 @@ EXTRA contains additional information." "_" name)) +(defun ellama-stringify-prompt (prompt) + "Convert prompt specification PROMPT to a string. +Useful for e.g. naming Ellama buffers." + (cond ((stringp prompt) prompt) + ((llm-multipart-p prompt) (ellama-stringify-prompt (llm-multipart-parts prompt))) + ((listp prompt) (let ((str "")) + (dolist (p prompt) + (setq str + (concat str + (if (stringp p) p (format "%s" p)) + "\n"))) + str)) + (t (error "PROMPT should be a string, list, or `llm-multipart', but was %s" prompt)))) + (defun ellama-generate-name-by-words (provider action prompt) "Generate name for ACTION by PROVIDER by getting first N words from PROMPT." (let* ((cleaned-prompt (replace-regexp-in-string "/" "_" prompt)) @@ -811,7 +842,8 @@ EXTRA contains additional information." (defun ellama-generate-name (provider action prompt) "Generate name for ellama ACTION by PROVIDER according to PROMPT." - (ellama--fix-file-name (funcall ellama-naming-scheme provider action prompt))) + (ellama--fix-file-name + (funcall ellama-naming-scheme provider action (ellama-stringify-prompt prompt)))) (defun ellama-get-nick-prefix-for-mode () "Return preferred header prefix char based om the current mode. @@ -1327,95 +1359,96 @@ failure (with BUFFER current). (declare-function spinner-start "ext:spinner") (declare-function spinner-stop "ext:spinner") (declare-function ellama-context-prompt-with-context "ellama-context") - (let* ((session-id (plist-get args :session-id)) - (session (or (plist-get args :session) - (when session-id - (with-current-buffer (ellama-get-session-buffer session-id) - ellama--current-session)))) - (provider (if session - (ellama-session-provider session) - (or (plist-get args :provider) - ellama-provider - (ellama-get-first-ollama-chat-model)))) - (buffer (or (plist-get args :buffer) - (when (ellama-session-p session) - (ellama-get-session-buffer (ellama-session-id session))) - (current-buffer))) - (reasoning-buffer (get-buffer-create - (concat (make-temp-name "*ellama-reasoning-") "*"))) - (point (or (plist-get args :point) - (with-current-buffer buffer (point)))) - (filter (or (plist-get args :filter) #'identity)) - (errcb (or (plist-get args :on-error) - (lambda (msg) - (error "Error calling the LLM: %s" msg)))) - (donecb (or (plist-get args :on-done) #'ignore)) - (prompt-with-ctx (ellama-context-prompt-with-context prompt)) - (system (or (plist-get args :system) - ellama-global-system)) - (llm-prompt (if session - (if (llm-chat-prompt-p (ellama-session-prompt session)) - (progn - (llm-chat-prompt-append-response - (ellama-session-prompt session) - prompt-with-ctx) - (when system - (llm-chat-prompt-append-response - (ellama-session-prompt session) - system 'system)) - (ellama-session-prompt session)) - (setf (ellama-session-prompt session) - (llm-make-chat-prompt prompt-with-ctx :context system))) - (llm-make-chat-prompt prompt-with-ctx :context system)))) + (let* ((prompt (if (listp prompt) (apply #'llm-make-multipart prompt) prompt)) + (session-id (plist-get args :session-id)) + (session (or (plist-get args :session) + (when session-id + (with-current-buffer (ellama-get-session-buffer session-id) + ellama--current-session)))) + (provider (if session + (ellama-session-provider session) + (or (plist-get args :provider) + ellama-provider + (ellama-get-first-ollama-chat-model)))) + (buffer (or (plist-get args :buffer) + (when (ellama-session-p session) + (ellama-get-session-buffer (ellama-session-id session))) + (current-buffer))) + (reasoning-buffer (get-buffer-create + (concat (make-temp-name "*ellama-reasoning-") "*"))) + (point (or (plist-get args :point) + (with-current-buffer buffer (point)))) + (filter (or (plist-get args :filter) #'identity)) + (errcb (or (plist-get args :on-error) + (lambda (msg) + (error "Error calling the LLM: %s" msg)))) + (donecb (or (plist-get args :on-done) #'ignore)) + (prompt-with-ctx (ellama-context-prompt-with-context prompt)) + (system (or (plist-get args :system) + ellama-global-system)) + (llm-prompt (if session + (if (llm-chat-prompt-p (ellama-session-prompt session)) + (progn + (llm-chat-prompt-append-response + (ellama-session-prompt session) + prompt-with-ctx) + (when system + (llm-chat-prompt-append-response + (ellama-session-prompt session) + system 'system)) + (ellama-session-prompt session)) + (setf (ellama-session-prompt session) + (llm-make-chat-prompt prompt-with-ctx :context system))) + (llm-make-chat-prompt prompt-with-ctx :context system)))) (with-current-buffer reasoning-buffer (org-mode)) (with-current-buffer buffer (ellama-request-mode +1) (let* ((insert-text - (ellama--insert buffer point filter)) - (insert-reasoning - (ellama--insert reasoning-buffer nil #'ellama--translate-markdown-to-org-filter))) - (setq ellama--change-group (prepare-change-group)) - (activate-change-group ellama--change-group) - (when ellama-spinner-enabled - (require 'spinner) - (spinner-start ellama-spinner-type)) - (let* ((handler (ellama--handle-partial insert-text insert-reasoning reasoning-buffer)) - (request (llm-chat-streaming - provider - llm-prompt - handler - (lambda (response) - (let ((text (plist-get response :text)) - (reasoning (plist-get response :reasoning))) - (funcall handler response) - (when (or ellama--current-session - (not reasoning)) - (kill-buffer reasoning-buffer)) - (with-current-buffer buffer - (accept-change-group ellama--change-group) - (when ellama-spinner-enabled - (spinner-stop)) - (if (and (listp donecb) - (functionp (car donecb))) - (mapc (lambda (fn) (funcall fn text)) - donecb) - (funcall donecb text)) - (when ellama-session-hide-org-quotes - (ellama-collapse-org-quotes)) - (setq ellama--current-request nil) - (ellama-request-mode -1)))) - (lambda (_ msg) - (with-current-buffer buffer - (cancel-change-group ellama--change-group) - (when ellama-spinner-enabled - (spinner-stop)) - (funcall errcb msg) - (setq ellama--current-request nil) - (ellama-request-mode -1))) - t))) - (with-current-buffer buffer - (setq ellama--current-request request))))))) + (ellama--insert buffer point filter)) + (insert-reasoning + (ellama--insert reasoning-buffer nil #'ellama--translate-markdown-to-org-filter))) + (setq ellama--change-group (prepare-change-group)) + (activate-change-group ellama--change-group) + (when ellama-spinner-enabled + (require 'spinner) + (spinner-start ellama-spinner-type)) + (let* ((handler (ellama--handle-partial insert-text insert-reasoning reasoning-buffer)) + (request (llm-chat-streaming + provider + llm-prompt + handler + (lambda (response) + (let ((text (plist-get response :text)) + (reasoning (plist-get response :reasoning))) + (funcall handler response) + (when (or ellama--current-session + (not reasoning)) + (kill-buffer reasoning-buffer)) + (with-current-buffer buffer + (accept-change-group ellama--change-group) + (when ellama-spinner-enabled + (spinner-stop)) + (if (and (listp donecb) + (functionp (car donecb))) + (mapc (lambda (fn) (funcall fn text)) + donecb) + (funcall donecb text)) + (when ellama-session-hide-org-quotes + (ellama-collapse-org-quotes)) + (setq ellama--current-request nil) + (ellama-request-mode -1)))) + (lambda (_ msg) + (with-current-buffer buffer + (cancel-change-group ellama--change-group) + (when ellama-spinner-enabled + (spinner-stop)) + (funcall errcb msg) + (setq ellama--current-request nil) + (ellama-request-mode -1))) + t))) + (with-current-buffer buffer + (setq ellama--current-request request))))))) (defun ellama-chain (initial-prompt forms &optional acc) "Call chain of FORMS on INITIAL-PROMPT. @@ -2024,6 +2057,19 @@ ARGS contains keys for fine control. ellama-provider (ellama-get-first-ollama-chat-model)))))) +(defun ellama-summarize-image (filepath) + "Summarize the image at FILEPATH. +`ellama-summarization-provider' must support image inputs for this function." + (interactive (list (read-file-name "Provide the image path"))) + (ellama-instant + (list (create-image (expand-file-name filepath))) + :system + ellama-multimodal-summarize-prompt-template + :provider + (or ellama-summarization-provider + ellama-provider + (ellama-get-first-ollama-chat-model)))) + ;;;###autoload (defun ellama-code-review (&optional create-session &rest args) "Review code in selected region or current buffer.