diff --git a/src/backend/src/modules/puterai/AIChatService.js b/src/backend/src/modules/puterai/AIChatService.js index 0cca9f132..8b2b867af 100644 --- a/src/backend/src/modules/puterai/AIChatService.js +++ b/src/backend/src/modules/puterai/AIChatService.js @@ -6,6 +6,7 @@ const { DB_WRITE } = require("../../services/database/consts"); const { TypeSpec } = require("../../services/drivers/meta/Construct"); const { TypedValue } = require("../../services/drivers/meta/Runtime"); const { Context } = require("../../util/context"); +const { AsModeration } = require("./lib/AsModeration"); // Maximum number of fallback attempts when a model fails, including the first attempt const MAX_FALLBACKS = 3 + 1; // includes first attempt @@ -278,6 +279,7 @@ class AIChatService extends BaseService { intended_service, parameters }; + await svc_event.emit('ai.prompt.validate', event); if ( ! event.allow ) { test_mode = true; } @@ -489,11 +491,6 @@ class AIChatService extends BaseService { * Returns true if OpenAI service is unavailable or all messages pass moderation. */ async moderate ({ messages }) { - const svc_openai = this.services.get('openai-completion'); - - // We can't use moderation of openai service isn't available - if ( ! svc_openai ) return true; - for ( const msg of messages ) { const texts = []; if ( typeof msg.content === 'string' ) texts.push(msg.content); @@ -508,8 +505,41 @@ class AIChatService extends BaseService { const fulltext = texts.join('\n'); - const mod_result = await svc_openai.check_moderation(fulltext); - if ( mod_result.flagged ) return false; + let mod_last_error = null; + let mod_result = null; + try { + const svc_openai = this.services.get('openai-completion'); + mod_result = await svc_openai.check_moderation(fulltext); + if ( mod_result.flagged ) return false; + continue; + } catch (e) { + console.error(e); + mod_last_error = e; + } + try { + const svc_claude = this.services.get('claude'); + const chat = svc_claude.as('puter-chat-completion'); + const mod = new AsModeration({ + chat, + model: 'claude-3-haiku-20240307', + }) + if ( ! await mod.moderate(fulltext) ) { + return false; + } + mod_last_error = null; + continue; + } catch (e) { + console.error(e); + mod_last_error = e; + } + + if ( mod_last_error ) { + this.log.error('moderation error', { + fulltext, + mod_last_error, + }); + throw new Error('no working moderation service'); + } } return true; } diff --git a/src/backend/src/modules/puterai/FakeChatService.js b/src/backend/src/modules/puterai/FakeChatService.js index 330805530..6855aedd2 100644 --- a/src/backend/src/modules/puterai/FakeChatService.js +++ b/src/backend/src/modules/puterai/FakeChatService.js @@ -65,6 +65,10 @@ class FakeChatService extends BaseService { "output_tokens": 1 } }, + "usage": { + "input_tokens": 0, + "output_tokens": 1 + }, "logprobs": null, "finish_reason": "stop" } diff --git a/src/backend/src/modules/puterai/lib/AsModeration.js b/src/backend/src/modules/puterai/lib/AsModeration.js new file mode 100644 index 000000000..2f20c76d4 --- /dev/null +++ b/src/backend/src/modules/puterai/lib/AsModeration.js @@ -0,0 +1,77 @@ +const { default: dedent } = require("dedent"); + +class AsModeration { + constructor ({ chat, model }) { + this.chat = chat; + this.model = model; + } + + async moderate (text) { + // copied from Anthropic documentation + // https://docs.anthropic.com/en/docs/about-claude/use-case-guides/content-moderation + const unsafe_categories = [ + 'Child Exploitation', + 'Conspiracy Theories', + 'Hate', + 'Indiscriminate Weapons', + 'Intellectual Property', + 'Non-Violent Crimes', + 'Privacy', + 'Self-Harm', + 'Sex Crimes', + 'Sexual Content', + 'Specialized Advice', + 'Violent Crimes' + ]; + + const unsafe_category_str = unsafe_categories.join('\n'); + + const assessment_prompt = dedent(` + Determine whether the following message warrants moderation, + based on the unsafe categories outlined below. + + Message: + + ${text + .replace('<', '<') + .replace('>', '>') + } + + + Unsafe Categories: + + ${unsafe_category_str} + + + Respond with ONLY a JSON object, using the format below: + {{ + "violation": , + "categories": [Comma-separated list of violated categories], + "explanation": [Optional. Only include if there is a violation.] + }} + `); + + const result = await this.chat.complete({ + messages: [ + { + role: 'user', + content: assessment_prompt, + } + ] + }); + + console.log('result???', require('util').inspect(result, { depth: null })); + + const str = result.message?.content?.[0]?.text ?? + result.messages?.[0]?.content?.[0]?.text ?? + '{ "violation": true }'; + + const parsed = JSON.parse(str); + console.log('parsed?', parsed); + return ! parsed.violation; + } +} + +module.exports = { + AsModeration, +}; diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 5a94441a8..14eb58e1c 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -169,7 +169,7 @@ -
+
diff --git a/src/dev-center/js/dev-center.js b/src/dev-center/js/dev-center.js index c6eab46c7..31c9670f2 100644 --- a/src/dev-center/js/dev-center.js +++ b/src/dev-center/js/dev-center.js @@ -518,9 +518,11 @@ function generate_edit_app_section(app) {
-
+
-
App has been successfully updated.×
+
App has been successfully updated.× +

Give it a try!

+

Basic

@@ -612,9 +614,10 @@ function generate_edit_app_section(app) {

credentialless attribute for the iframe tag.

-
- - +
+ + +
` diff --git a/src/gui/src/i18n/translations/fr.js b/src/gui/src/i18n/translations/fr.js index b9219b1ff..cd5066163 100644 --- a/src/gui/src/i18n/translations/fr.js +++ b/src/gui/src/i18n/translations/fr.js @@ -364,21 +364,21 @@ const fr = { "This user already has access to this item": 'Cet utilisateur à déja accès à cet élément', // ---------------------------------------- - // Missing translations: + // translations: // ---------------------------------------- - "billing.change_payment_method": "Modifier", // In English: "Change" + "billing.change_payment_method": "Modifier le mode de paiement", // In English: "Change" "billing.cancel": "Annuler", // In English: "Cancel" "billing.download_invoice": "Télécharger la facture", // In English: "Download" - "billing.payment_method": "Méthode de paiement", // In English: "Payment Method" - "billing.payment_method_updated": "Méthode de paiement mise à jour !", // In English: "Payment method updated!" - "billing.confirm_payment_method": "Confirmer la méthode de paiement", // In English: "Confirm Payment Method" + "billing.payment_method": "Mode de paiement", // In English: "Payment Method" + "billing.payment_method_updated": "Mode de paiement mis à jour !", // In English: "Payment method updated!" + "billing.confirm_payment_method": "Confirmer le mode de paiement", // In English: "Confirm Payment Method" "billing.payment_history": "Historique des paiements", // In English: "Payment History" "billing.refunded": "Remboursé", // In English: "Refunded" "billing.paid": "Payé", // In English: "Paid" "billing.ok": "OK", // In English: "OK" "billing.resume_subscription": "Reprendre l'abonnement", // In English: "Resume Subscription" "billing.subscription_cancelled": "Votre abonnement a été annulé.", // In English: "Your subscription has been canceled." - "billing.subscription_cancelled_description": "Vous conserverez l'accès à votre abonnement jusqu'à la fin de cette période de facturation.", // In English: "You will still have access to your subscription until the end of this billing period." + "billing.subscription_cancelled_description": "Vous aurez toujours accès à votre abonnement jusqu'à la fin de cette période de facturation.", // In English: "You will still have access to your subscription until the end of this billing period." "billing.offering.free": "Gratuit", // In English: "Free" "billing.offering.pro": "Professionnel", // In English: "Professional" "billing.offering.business": "Entreprise", // In English: "Business" @@ -386,24 +386,24 @@ const fr = { "billing.ai_access": "Accès à l'IA", // In English: "AI Access" "billing.bandwidth": "Bande passante", // In English: "Bandwidth" "billing.apps_and_games": "Applications et jeux", // In English: "Apps & Games" - "billing.upgrade_to_pro": "Passer à la version %strong%", // In English: "Upgrade to %strong%" - "billing.switch_to": "Optez pour %strong%", // In English: "Switch to %strong%" - "billing.payment_setup": "Configuration des paiements", // In English: "Payment Setup" + "billing.upgrade_to_pro": "Passer à %strong%", // In English: "Upgrade to %strong%" + "billing.switch_to": "Passer à %strong%", // In English: "Switch to %strong%" + "billing.payment_setup": "Configuration du paiement", // In English: "Payment Setup" "billing.back": "Retour", // In English: "Back" - "billing.you_are_now_subscribed_to": "Vous êtes désormais abonné au niveau %strong%.", // In English: "You are now subscribed to %strong% tier." - "billing.you_are_now_subscribed_to_without_tier": "Vous êtes désormais abonné", // In English: "You are now subscribed" - "billing.subscription_cancellation_confirmation": "Êtes-vous certain de vouloir annuler votre abonnement ?", // In English: "Are you sure you want to cancel your subscription?" + "billing.you_are_now_subscribed_to": "Vous êtes maintenant abonné au niveau %strong%.", // In English: "You are now subscribed to %strong% tier." + "billing.you_are_now_subscribed_to_without_tier": "Vous êtes maintenant abonné", // In English: "You are now subscribed" + "billing.subscription_cancellation_confirmation": "Êtes-vous sûr de vouloir annuler votre abonnement ?", // In English: "Are you sure you want to cancel your subscription?" "billing.subscription_setup": "Configuration de l'abonnement", // In English: "Subscription Setup" - "billing.cancel_it": "Annuler", // In English: "Cancel It" - "billing.keep_it": "Conserver", // In English: "Keep It" - "billing.subscription_resumed": "Votre abonnement %strong% a été réactivé !", // In English: "Your %strong% subscription has been resumed!" + "billing.cancel_it": "L'annuler", // In English: "Cancel It" + "billing.keep_it": "Le conserver", // In English: "Keep It" + "billing.subscription_resumed": "Votre abonnement %strong% a été repris !", // In English: "Your %strong% subscription has been resumed!" "billing.upgrade_now": "Mettre à niveau maintenant", // In English: "Upgrade Now" "billing.upgrade": "Mettre à niveau", // In English: "Upgrade" "billing.currently_on_free_plan": "Vous êtes actuellement sur le plan gratuit.", // In English: "You are currently on the free plan." "billing.download_receipt": "Télécharger le reçu", // In English: "Download Receipt" - "billing.subscription_check_error": "Un problème est survenu lors de la vérification de votre abonnement.", // In English: "A problem occurred while checking your subscription status." - "billing.email_confirmation_needed": "Votre e-mail n'a pas été confirmé. Nous vous enverrons un code pour le confirmer maintenant.", // In English: "Your email has not been confirmed. We'll send you a code to confirm it now." - "billing.sub_cancelled_but_valid_until": "Vous avez annulé votre abonnement, mais il restera actif jusqu'à la fin de la période de facturation. Vous ne serez pas facturé à nouveau, sauf si vous vous réabonnez.", // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe." + "billing.subscription_check_error": "Un problème est survenu lors de la vérification de votre statut d'abonnement.", // In English: "A problem occurred while checking your subscription status." + "billing.email_confirmation_needed": "Votre e-mail n'a pas été confirmé. Nous allons vous envoyer un code pour le confirmer maintenant.", // In English: "Your email has not been confirmed. We'll send you a code to confirm it now." + "billing.sub_cancelled_but_valid_until": "Vous avez annulé votre abonnement et il passera automatiquement au niveau gratuit à la fin de la période de facturation. Vous ne serez pas facturé à nouveau sauf si vous vous réabonnez.", // In English: "You have cancelled your subscription and it will automatically switch to the free tier at the end of the billing period. You will not be charged again unless you re-subscribe." "billing.current_plan_until_end_of_period": "Votre plan actuel jusqu'à la fin de cette période de facturation.", // In English: "Your current plan until the end of this billing period." "billing.current_plan": "Plan actuel", // In English: "Current plan" "billing.cancelled_subscription_tier": "Abonnement annulé (%%)", // In English: "Cancelled Subscription (%%)" @@ -411,8 +411,7 @@ const fr = { "billing.limited": "Limité", // In English: "Limited" "billing.expanded": "Étendu", // In English: "Expanded" "billing.accelerated": "Accéléré", // In English: "Accelerated" - "billing.enjoy_msg": "Profitez de %% de stockage cloud, ainsi que d'autres avantages." // In English: "Enjoy %% of Cloud Storage plus other benefits." - + "billing.enjoy_msg": "Profitez de %% de stockage cloud et d'autres avantages.", // In English: "Enjoy %% of Cloud Storage plus other benefits." } }; diff --git a/src/gui/src/i18n/translations/ko.js b/src/gui/src/i18n/translations/ko.js index a0ee7bf5e..800005d7c 100644 --- a/src/gui/src/i18n/translations/ko.js +++ b/src/gui/src/i18n/translations/ko.js @@ -435,6 +435,7 @@ const ko = { "billing.current_plan_until_end_of_period": "청구 기간이 끝날 때까지 유지되는 현재 플랜입니다.", // In English: "Your current plan until the end of this billing period." "billing.current_plan": "현재 플랜", // In English: "Current plan" ; depending on the context you could use: "구독 중인 플랜" (plan you are subscribed to) + "billing.cancelled_subscription_tier": "취소된 구독 (%%)", // In English: "Cancelled Subscription (%%)" "billing.manage": "관리", // In English: "Manage" "billing.limited": "제한됨", // In English: "Limited"