diff --git a/FAQ.md b/FAQ.md index 4740e24ff0..0fddbdc813 100644 --- a/FAQ.md +++ b/FAQ.md @@ -2905,6 +2905,7 @@ The following extra functions are available: * *Jsoup()* (returns an array of selected strings; since version 1.2179) * *Size(array)* (returns the number of items in an array; since version 1.2179) * *knownContact()* (returns a boolean indicating that the from/reply-to address is in the Android address book or in the local contacts database) +* *AI(prompt)* (run interference with the configured AI using the specified prompt, returning the result as a string; since version 1.2243) Example conditions: diff --git a/app/src/main/java/eu/faircode/email/AI.java b/app/src/main/java/eu/faircode/email/AI.java index ceade611d3..7a024bc0a8 100644 --- a/app/src/main/java/eu/faircode/email/AI.java +++ b/app/src/main/java/eu/faircode/email/AI.java @@ -26,6 +26,7 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import org.json.JSONException; @@ -78,6 +79,11 @@ public class AI { } } + return completeChat(context, id, body, reply, templatePrompt); + } + + @NonNull + static Spanned completeChat(Context context, long id, CharSequence body, String reply, String prompt) throws JSONException, IOException { StringBuilder sb = new StringBuilder(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (OpenAI.isAvailable(context)) { @@ -94,9 +100,9 @@ public class AI { new OpenAI.Content(OpenAI.CONTENT_TEXT, systemPrompt)})); if (reply == null) { - if (templatePrompt != null) + if (prompt != null) messages.add(new OpenAI.Message(OpenAI.USER, new OpenAI.Content[]{ - new OpenAI.Content(OpenAI.CONTENT_TEXT, templatePrompt)})); + new OpenAI.Content(OpenAI.CONTENT_TEXT, prompt)})); if (body instanceof Spannable && multimodal) messages.add(new OpenAI.Message(OpenAI.USER, OpenAI.Content.get((Spannable) body, id, context))); @@ -104,10 +110,10 @@ public class AI { messages.add(new OpenAI.Message(OpenAI.USER, new OpenAI.Content[]{ new OpenAI.Content(OpenAI.CONTENT_TEXT, body.toString())})); } else { - if (templatePrompt == null && body.length() > 0) - templatePrompt = body.toString(); + if (prompt == null && body.length() > 0) + prompt = body.toString(); messages.add(new OpenAI.Message(OpenAI.USER, new OpenAI.Content[]{ - new OpenAI.Content(OpenAI.CONTENT_TEXT, templatePrompt == null ? defaultPrompt : templatePrompt)})); + new OpenAI.Content(OpenAI.CONTENT_TEXT, prompt == null ? defaultPrompt : prompt)})); messages.add(new OpenAI.Message(OpenAI.USER, new OpenAI.Content[]{ new OpenAI.Content(OpenAI.CONTENT_TEXT, reply)})); } @@ -132,14 +138,14 @@ public class AI { List messages = new ArrayList<>(); if (reply == null) { - if (templatePrompt != null) - messages.add(new Gemini.Message(Gemini.USER, new String[]{templatePrompt})); + if (prompt != null) + messages.add(new Gemini.Message(Gemini.USER, new String[]{prompt})); messages.add(new Gemini.Message(Gemini.USER, new String[]{Gemini.truncateParagraphs(body.toString())})); } else { - if (templatePrompt == null && body.length() > 0) - templatePrompt = body.toString(); - messages.add(new Gemini.Message(Gemini.USER, new String[]{templatePrompt == null ? defaultPrompt : templatePrompt})); + if (prompt == null && body.length() > 0) + prompt = body.toString(); + messages.add(new Gemini.Message(Gemini.USER, new String[]{prompt == null ? defaultPrompt : prompt})); messages.add(new Gemini.Message(Gemini.USER, new String[]{reply})); } diff --git a/app/src/main/java/eu/faircode/email/ExpressionHelper.java b/app/src/main/java/eu/faircode/email/ExpressionHelper.java index f0da6feb5d..ae23da6c2a 100644 --- a/app/src/main/java/eu/faircode/email/ExpressionHelper.java +++ b/app/src/main/java/eu/faircode/email/ExpressionHelper.java @@ -115,6 +115,7 @@ public class ExpressionHelper { JsoupFunction fJsoup = new JsoupFunction(context, message); SizeFunction fSize = new SizeFunction(); KnownFunction fKnown = new KnownFunction(context, message); + AIFunction fAI = new AIFunction(context, doc); ContainsOperator oContains = new ContainsOperator(false); ContainsOperator oMatches = new ContainsOperator(true); @@ -130,6 +131,7 @@ public class ExpressionHelper { configuration.getFunctionDictionary().addFunction("Jsoup", fJsoup); configuration.getFunctionDictionary().addFunction("Size", fSize); configuration.getFunctionDictionary().addFunction("knownContact", fKnown); + configuration.getFunctionDictionary().addFunction("AI", fAI); configuration.getOperatorDictionary().addOperator("Contains", oContains); configuration.getOperatorDictionary().addOperator("Matches", oMatches); @@ -179,6 +181,17 @@ public class ExpressionHelper { for (String variable : expression.getUsedVariables()) if ("text".equalsIgnoreCase(variable)) return true; + + expression.validate(); + for (ASTNode node : expression.getAllASTNodes()) { + Token token = node.getToken(); + Log.i("EXPR token=" + token.getType() + ":" + token.getValue()); + if (token.getType() == Token.TokenType.FUNCTION && + "AI".equalsIgnoreCase(token.getValue())) { + Log.i("EXPR needs body"); + return true; + } + } } catch (Throwable ex) { Log.e("EXPR", ex); } @@ -444,6 +457,36 @@ public class ExpressionHelper { } } + @FunctionParameter(name = "value") + public static class AIFunction extends AbstractFunction { + private final Context context; + private final Document doc; + + AIFunction(Context context, Document doc) { + this.context = context; + this.doc = doc; + } + + @Override + public EvaluationValue evaluate( + Expression expression, Token functionToken, EvaluationValue... parameterValues) { + String result = null; + + try { + if (doc != null && parameterValues.length == 1) { + String prompt = parameterValues[0].getStringValue(); + if (!TextUtils.isEmpty(prompt)) + result = AI.completeChat(context, -1L, doc.text(), null, prompt).toString(); + } + } catch (Throwable ex) { + Log.w(ex); + } + + Log.i("EXPR AI()=" + result); + return expression.convertValue(result); + } + } + @InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) public static class ContainsOperator extends AbstractOperator { private final boolean regex; diff --git a/index.html b/index.html index d9f4b84081..cd9ff0925d 100644 --- a/index.html +++ b/index.html @@ -8,19 +8,10 @@ @@ -1602,6 +1593,7 @@ X-Google-Original-From: Somebody <somebody+extra@example.org>
  • Jsoup() (returns an array of selected strings; since version 1.2179)
  • Size(array) (returns the number of items in an array; since version 1.2179)
  • knownContact() (returns a boolean indicating that the from/reply-to address is in the Android address book or in the local contacts database)
  • +
  • AI(prompt) (run interference with the configured AI using the specified prompt, returning the result as a string; since version 1.2243)
  • Example conditions:

    header("X-Mailer") contains "Open-Xchange" && from matches ".*service@.*"