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) {
-
-
-
-
+
+
+
+
`
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"